1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Setting to hide unrelated results from the FFZ Control Center when searching. This is now enabled by default.
* Added: Setting to control whether the height of the Emote Menu is expanded. This is now disabled by default.
* Changed: When searching in the FFZ Control Center, pills are displayed in the navigation tree showing how many matching results there are.
* Changed: Update the method used for searching for channels and games, hopefully resulting in more accurate results.
* Changed: When searching for users in an auto-complete field, display a check-mark for verified users.
* Changed: When searching for users in an auto-complete field, respect the user's preference for rounded avatars.
* Fixed: Lazy load Markdown when possible to save on initial download size.
This commit is contained in:
SirStendec 2021-05-13 15:54:21 -04:00
parent ff4bb24a9a
commit f0d68527b8
21 changed files with 435 additions and 164 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.22.4",
"version": "4.22.5",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

View file

@ -10,7 +10,7 @@
<markdown :source="t(item.desc_i18n_key, item.description)" />
</section>
<div
v-for="i in item.contents"
v-for="i in visibleContents"
:key="i.full_key"
:class="{'ffz-unmatched-item': showing && ! shouldShow(i)}"
>
@ -34,6 +34,16 @@ export default {
return this.shouldShow(this.item);
},
visibleContents() {
if ( ! this.item || ! this.item.contents )
return [];
if ( ! this.context.matches_only )
return this.item.contents;
return this.item.contents.filter(item => this.shouldShow(item));
},
classes() {
return [
'ffz--menu-container',

View file

@ -60,20 +60,21 @@
<template v-if="! item.contents || ! item.contents.length || item.always_list_pages">
<ul class="tw-border-t tw-pd-y-1">
<li
v-for="i in item.items"
v-for="i in visibleItems"
:key="i.full_key"
:class="{'ffz-unmatched-item': ! shouldShow(i)}"
class="tw-pd-x-1"
>
<a href="#" @click="$emit('change-item', i, false)">
{{ t(i.i18n_key, i.title) }}
<span v-if="i.unseen" class="tw-pill">{{ i.unseen }}</span>
<span v-if="filter" class="ffz-pill ffz-pill--overlay">{{ countMatches(i) }}</span>
<span v-else-if="i.unseen" class="ffz-pill ffz-pill--overlay">{{ i.unseen }}</span>
</a>
</li>
</ul>
</template>
<div
v-for="i in item.contents"
v-for="i in visibleContents"
:key="i.full_key"
:class="{'ffz-unmatched-item': ! shouldShow(i)}"
>
@ -107,6 +108,26 @@ export default {
}
return out;
},
visibleItems() {
if ( ! this.item || ! this.item.items )
return [];
if ( ! this.context.matches_only )
return this.item.items;
return this.item.items.filter(item => this.shouldShow(item));
},
visibleContents() {
if ( ! this.item || ! this.item.contents )
return [];
if ( ! this.context.matches_only )
return this.item.contents;
return this.item.contents.filter(item => this.shouldShow(item));
}
},
@ -118,6 +139,31 @@ export default {
return item.no_filter || item.search_terms.includes(this.filter);
},
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
return 0;
if ( seen && seen.has(item) )
return 0;
if ( ! seen )
seen = new Set;
seen.add(item);
let count = 0;
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
count++;
return count;
},
markSeen(item) {
this.$emit('mark-seen', item);
},

View file

@ -35,7 +35,10 @@
<span class="tw-flex-grow-1">
{{ t(item.i18n_key, item.title) }}
</span>
<span v-if="item.pill" class="ffz-pill ffz-pill--overlay">
<span v-if="filter" class="ffz-pill ffz-pill--overlay">
{{ countMatches(item) }}
</span>
<span v-else-if="item.pill" class="ffz-pill ffz-pill--overlay">
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill) : item.pill }}
</span>
<span v-else-if="item.unseen" class="ffz-pill ffz-pill--overlay">
@ -117,6 +120,31 @@ export default {
return false;
},
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
return 0;
if ( seen && seen.has(item) )
return 0;
if ( ! seen )
seen = new Set;
seen.add(item);
let count = 0;
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
count++;
return count;
},
containsCurrent(item) {
let i = this.currentItem;
while ( i ) {

View file

@ -203,6 +203,15 @@ export default class MainMenu extends Module {
force_seen: true
});
this.settings.add('ffz.search.matches-only', {
default: true,
ui: {
path: 'Appearance > Control Center >> Search',
title: 'Hide items that do not match completely when searching.',
component: 'setting-check-box'
}
});
this.on('settings:added-definition', (key, definition) => {
this._addDefinitionToTree(key, definition);
this.scheduleUpdate();
@ -834,6 +843,7 @@ export default class MainMenu extends Module {
can_proxy: context._context.can_proxy,
proxied: context._context.proxied,
has_update: this.has_update,
matches_only: settings.get('ffz.search.matches-only'),
mod_icons: context.get('context.chat.showModIcons'),
setProxied: val => {
@ -946,9 +956,14 @@ export default class MainMenu extends Module {
}
},
_update_settings() {
_c.matches_only = settings.get('ffz.search.matches-only');
},
_add_user() {
this._users++;
if ( this._users === 1 ) {
settings.on(':changed:ffz.search.matches-only', this._update_settings, this);
settings.on(':profile-toggled', this._profile_toggled, this);
settings.on(':profile-created', this._profile_created, this);
settings.on(':profile-changed', this._profile_changed, this);
@ -963,6 +978,7 @@ export default class MainMenu extends Module {
_remove_user() {
this._users--;
if ( this._users === 0 ) {
settings.off(':changed:ffz.search.matches-only', this._update_settings, this);
settings.off(':profile-toggled', this._profile_toggled, this);
settings.off(':profile-created', this._profile_created, this);
settings.off(':profile-changed', this._profile_changed, this);

View file

@ -9,6 +9,7 @@ import {has, maybe_call, once} from 'utilities/object';
import Tooltip from 'utilities/tooltip';
import Module from 'utilities/module';
import awaitMD, {getMD} from 'utilities/markdown';
export default class TooltipProvider extends Module {
constructor(...args) {
@ -60,9 +61,9 @@ export default class TooltipProvider extends Module {
this.types.markdown = (target, tip) => {
tip.add_class = 'ffz-tooltip--markdown';
const md = this.getMarkdown();
const md = getMD();
if ( ! md )
return this.loadMarkdown().then(md => md.render(target.dataset.title));
return awaitMD().then(md => md.render(target.dataset.title));
return md.render(target.dataset.title);
};
@ -71,46 +72,8 @@ export default class TooltipProvider extends Module {
this.types.html = target => target.dataset.title;
this.onFSChange = this.onFSChange.bind(this);
this.loadMarkdown = once(this.loadMarkdown);
}
getMarkdown(callback) {
if ( this._md )
return this._md;
if ( callback )
this.loadMarkdown().then(md => callback(md));
}
async loadMarkdown() { // eslint-disable-line class-methods-use-this
if ( this._md )
return this._md;
const [MD, MILA] = await Promise.all([
import(/* webpackChunkName: 'markdown' */ 'markdown-it'),
import(/* webpackChunkName: 'markdown' */ 'markdown-it-link-attributes')
]);
const md = this._md = new MD.default({
html: false,
linkify: true
});
md.use(MILA.default, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
return md;
}
onEnable() {
const container = this.getRoot();

View file

@ -26,26 +26,7 @@
class="tw-flex-grow-1"
@selected="onSelected"
>
<div class="tw-pd-x-1 tw-pd-y-05">
<div class="tw-card tw-relative">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
<aspect :ratio="1/1.33">
<img
:alt="slot.item.displayName"
:src="slot.item.boxArtURL"
class="tw-image"
>
</aspect>
</div>
<div class="tw-card-body tw-overflow-hidden tw-relative">
<p class="tw-pd-x-1">
{{ slot.item.displayName }}
</p>
</div>
</div>
</div>
</div>
<autocomplete-game :game="slot.item" />
</autocomplete>
</div>
</section>

View file

@ -28,26 +28,7 @@
class="tw-flex-grow-1"
@selected="onSelected"
>
<div class="tw-pd-x-1 tw-pd-y-05">
<div class="tw-card tw-relative">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
<aspect :ratio="1">
<img
:alt="slot.item.displayName"
:src="slot.item.profileImageURL"
class="tw-image"
>
</aspect>
</div>
<div class="tw-card-body tw-overflow-hidden tw-relative">
<p class="tw-pd-x-1">
{{ slot.item.displayName }}
</p>
</div>
</div>
</div>
</div>
<autocomplete-user :user="slot.item" />
</autocomplete>
</div>
</section>

View file

@ -222,6 +222,15 @@ export default class EmoteMenu extends Module {
}
});
this.settings.add('chat.emote-menu.tall', {
default: false,
ui: {
path: 'Chat > Emote Menu >> Appearance',
title: 'Use extra height for the emote menu.',
component: 'setting-check-box'
}
});
this.settings.add('chat.emote-menu.show-heading', {
default: 1,
ui: {
@ -1066,6 +1075,7 @@ export default class EmoteMenu extends Module {
quickNav: t.chat.context.get('chat.emote-menu.show-quick-nav'),
animated: t.chat.context.get('chat.emotes.animated'),
showHeading: t.chat.context.get('chat.emote-menu.show-heading'),
tall: t.chat.context.get('chat.emote-menu.tall'),
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
@ -1217,6 +1227,7 @@ export default class EmoteMenu extends Module {
t.chat.context.on('changed:chat.emote-menu.show-heading', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.show-search', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.tall', this.updateSettingState, this);
window.ffz_menu = this;
}
@ -1231,6 +1242,7 @@ export default class EmoteMenu extends Module {
t.chat.context.off('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.show-search', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.tall', this.updateSettingState, this);
if ( window.ffz_menu === this )
window.ffz_menu = null;
@ -1244,7 +1256,8 @@ export default class EmoteMenu extends Module {
showHeading: t.chat.context.get('chat.emote-menu.show-heading'),
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
showSearch: t.chat.context.get('chat.emote-menu.show-search')
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
tall: t.chat.context.get('chat.emote-menu.tall')
});
}
@ -2303,7 +2316,7 @@ export default class EmoteMenu extends Module {
return (<div class={`tw-block${this.props.visible ? '' : ' tw-hide'}`} style={{display: this.props.visible ? null : 'none !important'}}>
<div class="tw-absolute tw-attached tw-attached--right tw-attached--up">
<div
class={`ffz-balloon ffz-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${padding ? ' reduced-padding' : ''}`}
class={`ffz-balloon ffz-balloon--auto tw-inline-block tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2 ffz--emote-picker${this.state.tall ? ' ffz--emote-picker__tall' : ''}${padding ? ' reduced-padding' : ''}`}
data-a-target="emote-picker"
role="dialog"
>

View file

@ -9,7 +9,7 @@ import {SiteModule} from 'utilities/module';
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
import Twilight from 'site';
import getMD from 'src/utilities/markdown';
import awaitMD, {getMD} from 'utilities/markdown';
export default class MenuButton extends SiteModule {
@ -292,6 +292,12 @@ export default class MenuButton extends SiteModule {
/*if ( ! data.id )
data.id = generateUUID();*/
if ( data.markdown ) {
const md = getMD();
if ( ! md )
return awaitMD().then(() => this.addToast(data));
}
this.toasts.push(data);
// TODO: Sort by ending time?
if ( this.toasts.length > 5 )
@ -385,6 +391,9 @@ export default class MenuButton extends SiteModule {
}
let progress_bar = null;
const md = data.markdown ? getMD() : null;
if ( data.markdown && ! md )
return null;
if ( data.timeout ) {
const now = performance.now();

View file

@ -318,32 +318,44 @@
}
}*/
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
max-height: 30rem;
}
.emote-picker__nav-content-overflow,
.emote-picker__tab-content {
height: unset !important;
max-height: 50rem;
}
.whispers-thread {
overflow: visible !important;
}
@media only screen and (max-height: 750px) {
&:not(.ffz--emote-picker__tall) {
.emote-picker__nav-content-overflow {
height: 100% !important;
height: 30.5rem !important;
}
.emote-picker__nav-content-overflow-whisper {
height: 18rem !important;
}
}
&.ffz--emote-picker__tall {
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
max-height: 30rem;
}
.emote-picker__nav-content-overflow,
.emote-picker__tab-content {
#root & {
max-height: calc(100vh - 31rem);
height: unset !important;
max-height: 50rem;
}
.whispers-thread {
overflow: visible !important;
}
@media only screen and (max-height: 750px) {
.emote-picker__nav-content-overflow {
height: 100% !important;
}
max-height: calc(100vh - 26rem);
.emote-picker__nav-content-overflow,
.emote-picker__tab-content {
#root & {
max-height: calc(100vh - 31rem);
}
max-height: calc(100vh - 26rem);
}
}
}

View file

@ -0,0 +1,22 @@
<template functional>
<div class="tw-pd-x-1 tw-pd-y-05">
<div class="tw-card tw-relative">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden">
<aspect :ratio="1/1.33">
<img
:alt="props.game.displayName"
:src="props.game.boxArtURL"
class="tw-image"
>
</aspect>
</div>
<div class="tw-card-body tw-overflow-hidden tw-relative">
<p class="tw-pd-x-1">
{{ props.game.displayName }}
</p>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,35 @@
<template functional>
<div class="tw-pd-x-1 tw-pd-y-05">
<div class="tw-card tw-relative">
<div class="tw-align-items-center tw-flex tw-flex-nowrap tw-flex-row">
<div class="ffz-card-img ffz-card-img--size-3 tw-flex-shrink-0 tw-overflow-hidden tw-avatar">
<aspect :ratio="1">
<img
:alt="props.user.displayName"
:src="props.user.profileImageURL"
class="tw-image tw-image-avatar tw-border-radius-rounded"
>
</aspect>
</div>
<div class="tw-card-body tw-overflow-hidden tw-relative tw-flex tw-align-items-center tw-flex-grow-1">
<div
v-if="props.user.login && props.user.displayName && props.user.login.trim() !== props.user.displayName.trim().toLowerCase()"
class="tw-flex tw-flex-column tw-mg-x-1 tw-flex-grow-1"
>
<p>{{ props.user.displayName }}</p>
<p class="tw-font-size-8 tw-c-text-alt-2">
({{ props.user.login }})
</p>
</div>
<p v-else class="tw-mg-x-1 tw-flex-grow-1">
{{ props.user.displayName }}
</p>
<figure
v-if="props.user.roles && props.user.roles.isPartner"
class="tw-c-text-link ffz-i-verified"
/>
</div>
</div>
</div>
</div>
</template>

View file

@ -129,6 +129,11 @@ export default {
logger: {
type: Object,
required: false
},
allowFilter: {
type: Boolean,
required: false,
default: true
}
},
@ -172,7 +177,7 @@ export default {
if ( this.errored )
return null;
if ( ! this.search || ! this.search.length )
if ( ! this.search || ! this.search.length || ! this.allowFilter )
return this.cachedItems;
const needle = this.search.toLowerCase();
@ -180,6 +185,9 @@ export default {
if ( typeof item.displayName === 'string' && item.displayName.toLowerCase().includes(needle) )
return true;
if ( typeof item.login === 'string' && item.login.toLowerCase().includes(needle) )
return true;
if ( typeof item.label === 'string' && item.label.toLowerCase().includes(needle) )
return true;

View file

@ -5,21 +5,31 @@
<script>
import getMD from 'utilities/markdown';
import awaitMD, {getMD} from 'utilities/markdown';
export default {
props: {
source: String
},
computed: {
md() {
return getMD();
},
data() {
return {
md: getMD()
}
},
computed: {
output() {
if ( ! this.md )
return '';
return this.md.render(this.source);
}
},
created() {
if ( ! this.md )
awaitMD().then(md => this.md = md);
}
}

View file

@ -19,7 +19,7 @@
@keyup.down="nextTab"
>
<div
v-for="(i, idx) in item.tabs"
v-for="(i, idx) in visibleTabs"
:id="'tab-for-' + i.full_key"
:key="i.full_key"
:aria-selected="selected === idx"
@ -30,7 +30,8 @@
@click="select(idx)"
>
{{ t(i.i18n_key, i.title) }}
<span v-if="i.unseen > 0" class="tw-pill">{{ i.unseen }}</span>
<span v-if="filter" class="ffz-pill ffz-pill--overlay">{{ countMatches(i) }}</span>
<span v-else-if="i.unseen" class="ffz-pill ffz-pill--overlay">{{ i.unseen }}</span>
</div>
</header>
<section
@ -45,7 +46,7 @@
{{ t(tab.desc_i18n_key, tab.description) }}
</section>
<div
v-for="i in tab.contents"
v-for="i in visibleContents"
:key="i.full_key"
:class="{'ffz-unmatched-item': showing && ! shouldShow(i)}"
>
@ -79,6 +80,26 @@ export default {
tab() {
return this.item.tabs[this.selected];
},
visibleTabs() {
if ( ! this.item || ! this.item.tabs )
return [];
if ( ! this.context.matches_only )
return this.item.tabs;
return this.item.tabs.filter(tab => this.shouldShow(tab));
},
visibleContents() {
if ( ! this.tab || ! this.tab.contents )
return [];
if ( ! this.context.matches_only )
return this.tab.contents;
return this.tab.contents.filter(item => this.shouldShow(item));
}
},
@ -97,6 +118,31 @@ export default {
},
methods: {
countMatches(item, seen) {
if ( ! this.filter || ! this.filter.length || ! item )
return 0;
if ( seen && seen.has(item) )
return 0;
if ( ! seen )
seen = new Set;
seen.add(item);
let count = 0;
for(const key of ['tabs', 'contents', 'items'])
if ( item[key] )
for(const thing of item[key])
count += this.countMatches(thing, seen);
if ( item.setting && item.search_terms && item.search_terms.includes(this.filter) )
count++;
return count;
},
focus() {
this.$el.querySelector('header').focus();
},

View file

@ -1,11 +1,12 @@
query FFZ_SearchCategory($query: String!, $first: Int, $after: String) {
searchFor(userQuery: $query, platform: "web", target: {index: GAME, cursor: $after, limit: $first}) {
games {
query FFZ_SearchCategory($query: String!, $first: Int, $after: Cursor) {
searchCategories(query: $query, first: $first, after: $after) {
totalCount
pageInfo {
hasNextPage
}
edges {
cursor
pageInfo {
hasNextPage
}
items {
node {
id
name
displayName

View file

@ -1,15 +1,19 @@
query FFZ_SearchUser($query: String!, $first: Int, $after: String) {
searchFor(userQuery: $query, platform: "web", target: {index: USER, cursor: $after, limit: $first}) {
users {
query FFZ_SearchUser($query: String!, $first: Int, $after: Cursor) {
searchUsers(userQuery: $query, first: $first, after: $after) {
totalCount
pageInfo {
hasNextPage
}
edges {
cursor
pageInfo {
hasNextPage
}
items {
node {
id
login
displayName
profileImageURL(width: 50)
roles {
isPartner
}
}
}
}

View file

@ -4,5 +4,8 @@ query FFZ_UserBulk($ids: [ID!], $logins: [String!]) {
login
displayName
profileImageURL(width: 50)
roles {
isPartner
}
}
}

View file

@ -1,11 +1,46 @@
'use strict';
import MD from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
import {parse as parse_path} from 'utilities/path-parser';
let MD, MILA, waiters;
let _md;
function loadMD() {
if ( MD )
return Promise.resolve(MD);
return new Promise((s,f) => {
if ( waiters )
return waiters.push([s,f]);
waiters = [[s,f]];
Promise.all([
import(/* webpackChunkName: 'markdown' */ 'markdown-it'),
import(/* webpackChunkName: 'markdown' */ 'markdown-it-link-attributes')
]).then(modules => {
console.log('loaded', modules);
MD = modules[0]?.default;
MILA = modules[1]?.default;
const waited = waiters;
waiters = null;
for(const pair of waited)
pair[0](MD);
}).catch(err => {
const waited = waiters;
waiters = null;
for(const pair of waited)
pair[1](err);
});
});
}
function SettingsLinks(md) {
const default_render = md.renderer.rules.link_open || this.defaultRender;
@ -73,8 +108,12 @@ SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
}
export default function getMD() {
export default async function awaitMD() {
if ( ! _md ) {
const MD = await loadMD();
if ( ! MD )
return null;
const md = _md = new MD({
html: false,
linkify: true
@ -94,3 +133,10 @@ export default function getMD() {
return _md;
}
export function getMD(callback) {
if ( _md )
return _md;
if ( callback )
awaitMD().then(md => callback(md));
}

View file

@ -10,6 +10,15 @@ import {get, debounce} from 'utilities/object';
const LANGUAGE_MATCHER = /^auto___lang_(\w+)$/;
/**
* PaginatedResult
*
* @typedef {Object} PaginatedResult
* @property {String} cursor A cursor usable to fetch the next page of results
* @property {Object[]} items This page of results
* @property {Boolean} finished Whether or not we have reached the end of results.
*/
/**
* TwitchData is a container for getting different types of Twitch data
* @class TwitchData
@ -99,28 +108,42 @@ export default class TwitchData extends Module {
// ========================================================================
/**
* Queries Apollo for categories matching the search argument
* @function getMatchingCategories
* @memberof TwitchData
* @async
* Find categories matching the search query
*
* @param {string} query - query text to match to a category
* @returns {Object} a collection of matches for the query string
*
* @example
*
* console.log(this.twitch_data.getMatchingCategories("siege"));
* @param {String} query The category name to match
* @param {Number} [first=15] How many results to return
* @param {String} [cursor=null] A cursor, to be used in fetching the
* next page of results.
* @returns {PaginatedResult} The results
*/
async getMatchingCategories(query) {
async getMatchingCategories(query, first = 15, cursor = null) {
const data = await this.queryApollo(
await import(/* webpackChunkName: 'queries' */ './data/search-category.gql'),
{ query }
{
query,
first,
cursor
}
);
const items = get('data.searchCategories.edges.@each.node', data) ?? [],
needle = query.toLowerCase();
if ( Array.isArray(items) )
items.sort((a,b) => {
const a_match = a && (a.name?.toLowerCase?.() === needle || a?.displayName?.toLowerCase?.() === needle),
b_match = a && (b.name?.toLowerCase?.() === needle || b?.displayName?.toLowerCase?.() === needle);
if ( a_match && ! b_match ) return -1;
if ( ! a_match && b_match ) return 1;
return 0;
});
return {
cursor: get('data.searchFor.games.cursor', data),
items: get('data.searchFor.games.items', data) || [],
finished: ! get('data.searchFor.games.pageInfo.hasNextPage', data)
cursor: get('data.searchCategories.edges.@last.cursor', data),
items,
finished: ! get('data.searchCategories.pageInfo.hasNextPage', data),
count: get('data.searchCategories.totalCount', data) || 0
};
}
@ -153,28 +176,42 @@ export default class TwitchData extends Module {
// ========================================================================
/**
* Queries Apollo for users matching the search argument
* @function getMatchingUsers
* @memberof TwitchData
* @async
* Find users matching the search query.
*
* @param {string} query - query text to match to a username
* @returns {Object} a collection of matches for the query string
*
* @example
*
* console.log(this.twitch_data.getMatchingUsers("ninja"));
* @param {String} query Text to match in the login or display name
* @param {Number} [first=15] How many results to return
* @param {String} [cursor=null] A cursor, to be used in fetching the next
* page of results.
* @returns {PaginatedResult} The results
*/
async getMatchingUsers(query) {
async getMatchingUsers(query, first = 15, cursor = null) {
const data = await this.queryApollo(
await import(/* webpackChunkName: 'queries' */ './data/search-user.gql'),
{ query }
{
query,
first,
cursor
}
);
const items = get('data.searchUsers.edges.@each.node', data) ?? [],
needle = query.toLowerCase();
if ( Array.isArray(items) )
items.sort((a,b) => {
const a_match = a && (a.login?.toLowerCase?.() === needle || a?.displayName?.toLowerCase?.() === needle),
b_match = a && (b.login?.toLowerCase?.() === needle || b?.displayName?.toLowerCase?.() === needle);
if ( a_match && ! b_match ) return -1;
if ( ! a_match && b_match ) return 1;
return 0;
});
return {
cursor: get('data.searchFor.users.cursor', data),
items: get('data.searchFor.users.items', data) || [],
finished: ! get('data.searchFor.users.pageInfo.hasNextPage', data)
cursor: get('data.searchUsers.edges.@last.cursor', data),
items,
finished: ! get('data.searchUsers.pageInfo.hasNextPage', data),
count: get('data.searchUsers.totalCount', data) || 0
};
}