1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-01 15:38:31 +00:00
* Changed: Use the new Twitch Data module for fetching stream up-time when it isn't known, rather than forcing queries to re-fetch.
* Changed: Add a method to Twitch Data for looking up stream up-time.
* Fixed: The autocompletion component should not swallow key-presses when modifier keys are being held.
* Fixed: Issue when comparing against `null` with `deep_equals`.
This commit is contained in:
SirStendec 2019-06-15 03:58:06 -04:00
parent 275248ca36
commit 80148e5579
16 changed files with 363 additions and 84 deletions

View file

@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 4, revision: 1,
major: 4, minor: 4, revision: 2,
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -23,10 +23,10 @@ export default class BrowsePopular extends SiteModule {
onEnable() {
// Popular Directory Channel Cards
this.apollo.ensureQuery(
/*this.apollo.ensureQuery(
'BrowsePage_Popular',
'data.streams.edges.0.node.createdAt'
);
);*/
}
modifyStreams(res) { // eslint-disable-line class-methods-use-this

View file

@ -169,7 +169,7 @@ export default class Following extends SiteModule {
}
ensureQueries () {
this.apollo.ensureQuery(
/*this.apollo.ensureQuery(
'FollowedChannels_RENAME2',
'data.currentUser.followedLiveUsers.nodes.0.stream.createdAt'
);
@ -182,7 +182,7 @@ export default class Following extends SiteModule {
this.apollo.ensureQuery(
'RecommendedChannels',
'data.currentUser.recommendations.liveRecommendations.nodes.0.createdAt'
);
);*/
if ( this.router.current_name !== 'dir-following' )
return;
@ -197,11 +197,11 @@ export default class Following extends SiteModule {
get('data.currentUser.followedHosts.nodes.0.hosting.stream.createdAt', n) !== undefined
);
else if ( bit === 'live' )
/*else if ( bit === 'live' )
this.apollo.ensureQuery(
'FollowingLive_CurrentUser',
'data.currentUser.followedLiveUsers.nodes.0.stream.createdAt'
);
);*/
else if ( bit === 'hosts' )
this.apollo.ensureQuery(
@ -217,7 +217,7 @@ export default class Following extends SiteModule {
}
destroyHostMenu(event) {
if (!event || event && event.target && event.target.closest('.ffz-channel-selector-outer') === null && Date.now() > this.hostMenuBuffer) {
if (!event || ! this.hostMenu || event && event.target && event.target.closest('.ffz-channel-selector-outer') === null && Date.now() > this.hostMenuBuffer) {
this.hostMenuPopper && this.hostMenuPopper.destroy();
this.hostMenu && this.hostMenu.remove();
this.hostMenuPopper = this.hostMenu = undefined;

View file

@ -29,11 +29,11 @@ export default class Game extends SiteModule {
this.apollo.registerModifier('DirectoryPage_Game', GAME_QUERY);
this.apollo.registerModifier('DirectoryPage_Game', res => {
setTimeout(() =>
/*setTimeout(() =>
this.apollo.ensureQuery(
'DirectoryPage_Game',
'data.game.streams.edges.0.node.createdAt'
), 500);
), 500);*/
this.modifyStreams(res);
}, false);
@ -62,10 +62,10 @@ export default class Game extends SiteModule {
updateGameHeader(inst) {
this.updateButtons(inst);
this.apollo.ensureQuery(
/*this.apollo.ensureQuery(
'DirectoryPage_Game',
'data.game.streams.edges.0.node.createdAt'
);
);*/
}

View file

@ -38,6 +38,7 @@ export default class Directory extends SiteModule {
this.inject('site.apollo');
this.inject('site.css_tweaks');
this.inject('site.web_munch');
this.inject('site.twitch_data');
this.inject('i18n');
this.inject('settings');
@ -200,7 +201,7 @@ export default class Directory extends SiteModule {
this.DirectoryVideos.forceUpdate();
})
this.DirectoryCard.ready(cls => {
this.DirectoryCard.ready((cls, instances) => {
//const old_render = cls.prototype.render,
const old_render_iconic = cls.prototype.renderIconicImage,
old_render_titles = cls.prototype.renderTitles;
@ -272,13 +273,13 @@ export default class Directory extends SiteModule {
// Game Directory Channel Cards
// TODO: Better query handling.
this.apollo.ensureQuery(
/*this.apollo.ensureQuery(
'DirectoryPage_Game',
'data.game.streams.edges.0.node.createdAt'
);
);*/
//for(const inst of instances)
// this.updateCard(inst);
for(const inst of instances)
this.updateCard(inst);
});
this.DirectoryCard.on('update', this.updateCard, this);
@ -373,12 +374,33 @@ export default class Directory extends SiteModule {
updateUptime(inst, created_path) {
const container = this.fine.getChildNode(inst),
card = container && container.querySelector && container.querySelector('.preview-card-overlay'),
setting = this.settings.get('directory.uptime'),
created_at = inst.props && inst.props.createdAt || get(created_path, inst),
up_since = created_at && new Date(created_at),
setting = this.settings.get('directory.uptime');
if ( ! card || setting === 0 || ! inst.props || inst.props.viewCount || inst.props.animatedImageProps )
return this.clearUptime(inst);
let created_at = inst.props.createdAt || get(created_path, inst);
if ( ! created_at ) {
if ( inst.ffz_stream_meta === undefined ) {
inst.ffz_stream_meta = null;
this.twitch_data.getStreamMeta(inst.props.channelId, inst.props.channelLogin).then(data => {
inst.ffz_stream_meta = data;
this.updateUptime(inst, created_path);
});
}
if ( inst.ffz_stream_meta )
created_at = inst.ffz_stream_meta.createdAt;
}
if ( ! created_at )
return this.clearUptime(inst);
const up_since = created_at && new Date(created_at),
uptime = up_since && Math.floor((Date.now() - up_since) / 1000) || 0;
if ( ! card || setting === 0 || uptime < 1 )
if ( uptime < 1 )
return this.clearUptime(inst);
const up_text = duration_to_string(uptime, false, false, false, setting === 1);

View file

@ -305,6 +305,9 @@ export default {
},
onHome(event) {
if ( event.ctrlKey || event.shiftKey || event.altKey )
return;
if ( ! this.open )
return;
@ -316,6 +319,9 @@ export default {
},
onEnd(event) {
if ( event.ctrlKey || event.shiftKey || event.altKey )
return;
if ( ! this.open )
return;
@ -327,6 +333,9 @@ export default {
},
onUp(event) {
if ( event.ctrlKey || event.shiftKey || event.altKey )
return;
if ( ! this.open )
return;
@ -341,6 +350,9 @@ export default {
},
onDown(event) {
if ( event.ctrlKey || event.shiftKey || event.altKey )
return;
if ( ! this.open )
return;
@ -363,6 +375,9 @@ export default {
},
onEnter(event) {
if ( event.ctrlKey || event.shiftKey || event.altKey )
return;
if ( ! this.open )
return;

View file

@ -0,0 +1,10 @@
query FFZ_StreamFetch($ids: [ID!], $logins: [String!]) {
users(ids: $ids, logins: $logins) {
id
login
stream {
id
createdAt
}
}
}

View file

@ -0,0 +1,9 @@
query FFZ_SingleStream($id: ID, $login: String) {
user(id: $id, login: $login) {
id
stream {
id
createdAt
}
}
}

View file

@ -179,6 +179,8 @@ export function deep_equals(object, other, ignore_undefined = false, seen, other
return false;
if ( typeof object !== 'object' )
return false;
if ( (object === null) !== (other === null) )
return false;
if ( ! seen )
seen = new Set;

View file

@ -18,10 +18,14 @@ export default class TwitchData extends Module {
this.inject('site.apollo');
this.inject('site.web_munch');
this._waiting_stream_ids = new Map;
this._waiting_stream_logins = new Map;
this.tag_cache = new Map;
this._waiting_tags = new Map;
this._loadTags = debounce(this._loadTags.bind(this), 50);
this._loadTags = debounce(this._loadTags, 50);
this._loadStreams = debounce(this._loadStreams, 50);
}
queryApollo(query, variables, options) {
@ -56,7 +60,7 @@ export default class TwitchData extends Module {
return this._search;
const apollo = this.apollo.client,
core = this.listeners.getCore(),
core = this.site.getCore(),
search_module = this.web_munch.getModule('algolia-search'),
SearchClient = search_module && search_module.a;
@ -121,55 +125,220 @@ export default class TwitchData extends Module {
}
// ========================================================================
// Stream Up-Type (Uptime and Type, for Directory Purposes)
// ========================================================================
getStreamMeta(id, login) {
return new Promise(async (s, f) => {
if ( id ) {
if ( this._waiting_stream_ids.has(id) )
this._waiting_stream_ids.get(id).push([s, f]);
else
this._waiting_stream_ids.set(id, [[s, f]]);
} else if ( login ) {
if ( this._waiting_stream_logins.has(login) )
this._waiting_stream_logins.get(login).push([s, f]);
else
this._waiting_stream_logins.set(login, [[s, f]]);
} else
f('id and login cannot both be null');
if ( ! this._loading_streams )
this._loadStreams();
})
}
async _loadStreams() {
if ( this._loading_streams )
return;
this._loading_streams = true;
// Get the first 50... things.
const ids = [...this._waiting_stream_ids.keys()].slice(0, 50),
remaining = 50 - ids.length,
logins = remaining > 0 ? [...this._waiting_stream_logins.keys()].slice(0, remaining) : [];
let nodes;
try {
const data = await this.queryApollo({
query: require('./data/stream-fetch.gql'),
variables: {
ids: ids.length ? ids : null,
logins: logins.length ? logins : null
}
});
nodes = get('data.users', data);
} catch(err) {
for(const id of ids) {
const promises = this._waiting_stream_ids.get(id);
this._waiting_stream_ids.delete(id);
for(const pair of promises)
pair[1](err);
}
for(const login of logins) {
const promises = this._waiting_stream_logins.get(login);
this._waiting_stream_logins.delete(login);
for(const pair of promises)
pair[1](err);
}
return;
}
const id_set = new Set(ids),
login_set = new Set(logins);
if ( Array.isArray(nodes) )
for(const node of nodes) {
if ( ! node || ! node.id )
continue;
id_set.delete(node.id);
login_set.delete(node.login);
let promises = this._waiting_stream_ids.get(node.id);
if ( promises ) {
this._waiting_stream_ids.delete(node.id);
for(const pair of promises)
pair[0](node.stream);
}
promises = this._waiting_stream_logins.get(node.login);
if ( promises ) {
this._waiting_stream_logins.delete(node.login);
for(const pair of promises)
pair[0](node.stream);
}
}
for(const id of id_set) {
const promises = this._waiting_stream_ids.get(id);
if ( promises ) {
this._waiting_stream_ids.delete(id);
for(const pair of promises)
pair[0](null);
}
}
for(const login of login_set) {
const promises = this._waiting_stream_logins.get(login);
if ( promises ) {
this._waiting_stream_logins.delete(login);
for(const pair of promises)
pair[0](null);
}
}
this._loading_streams = false;
if ( this._waiting_stream_ids.size || this._waiting_stream_logins.size )
this._loadStreams();
}
// ========================================================================
// Tags
// ========================================================================
memorizeTag(node, dispatch = true) {
// We want properly formed tags.
if ( ! node || ! node.id || ! node.tagName || ! node.localizedName )
return;
let old = null;
if ( this.tag_cache.has(node.id) )
old = this.tag_cache.get(old);
const match = node.isLanguageTag && LANGUAGE_MATCHER.exec(node.tagName),
lang = match && match[1] || null;
const new_tag = {
id: node.id,
value: node.id,
is_language: node.isLanguageTag,
language: lang,
name: node.tagName,
label: node.localizedName
};
if ( node.localizedDescription )
new_tag.description = node.localizedDescription;
const tag = old ? Object.assign(old, new_tag) : new_tag;
this.tag_cache.set(tag.id, tag);
if ( dispatch && tag.description && this._waiting_tags.has(tag.id) ) {
const promises = this._waiting_tags.get(tag.id);
this._waiting_tags.delete(tag.id);
for(const pair of promises)
pair[0](tag);
}
return tag;
}
async _loadTags() {
if ( this._loading_tags )
return;
this._loading_tags = true;
const processing = this._waiting_tags;
this._waiting_tags = new Map;
// Get the first 50 tags.
const ids = [...this._waiting_tags.keys()].slice(0, 50);
let nodes
try {
const data = await this.queryApollo(
require('./data/tags-fetch.gql'),
{
ids: [...processing.keys()]
ids
}
);
const nodes = get('data.contentTags', data);
if ( Array.isArray(nodes) )
for(const node of nodes) {
const tag = {
id: node.id,
value: node.id,
is_language: node.isLanguageTag,
name: node.tagName,
label: node.localizedName,
description: node.localizedDescription
};
this.tag_cache.set(tag.id, tag);
const promises = processing.get(tag.id);
if ( promises )
for(const pair of promises)
pair[0](tag);
promises.delete(tag.id);
}
for(const promises of processing.values())
for(const pair of promises)
pair[0](null);
nodes = get('data.contentTags', data);
} catch(err) {
for(const promises of processing.values())
for(const id of ids) {
const promises = this._waiting_tags.get(id);
this._waiting_tags.delete(id);
for(const pair of promises)
pair[1](err);
}
return;
}
const id_set = new Set(ids);
if ( Array.isArray(nodes) )
for(const node of nodes) {
const tag = this.memorizeTag(node, false),
promises = this._waiting_tags.get(tag.id);
this._waiting_tags.delete(tag.id);
id_set.delete(tag.id);
if ( promises )
for(const pair of promises)
pair[0](tag);
}
for(const id of id_set) {
const promises = this._waiting_tags.get(id);
this._waiting_tags.delete(id);
for(const pair of promises)
pair[0](null);
}
this._loading_tags = false;
@ -179,6 +348,10 @@ export default class TwitchData extends Module {
}
getTag(id, want_description = false) {
// Make sure we weren't accidentally handed a tag object.
if ( id && id.id )
id = id.id;
if ( this.tag_cache.has(id) ) {
const out = this.tag_cache.get(id);
if ( out && (out.description || ! want_description) )
@ -197,6 +370,10 @@ export default class TwitchData extends Module {
}
getTagImmediate(id, callback, want_description = false) {
// Make sure we weren't accidentally handed a tag object.
if ( id && id.id )
id = id.id;
let out = null;
if ( this.tag_cache.has(id) )
out = this.tag_cache.get(id);
@ -223,17 +400,7 @@ export default class TwitchData extends Module {
continue;
seen.add(node.id);
const tag = {
id: node.id,
value: node.id,
is_language: node.isLanguageTag,
name: node.tagName,
label: node.localizedName,
description: node.localizedDescription
};
this.tag_cache.set(tag.id, tag);
out.push(tag);
out.push(this.memorizeTag(node));
}
return out;
@ -258,46 +425,89 @@ export default class TwitchData extends Module {
return out;
}
async getMatchingTags(query, locale) {
async getMatchingTags(query, locale, category = null) {
if ( ! locale )
locale = this.locale;
const data = await this.searchClient.queryForType(
'tag', query, generateUUID(), {
hitsPerPage: 100,
facetFilters: [
locale = locale.toLowerCase();
],
restrictSearchableAttributes: [
`localizations.${locale}`,
'tag_name'
]
}
);
let nodes;
if ( category ) {
const data = await this.searchClient.queryForType(
'stream_tag', query, generateUUID(), {
hitsPerPage: 100,
faceFilters: [
`category_id:${category}`
],
restrictSearchableAttributes: [
`localizations.${locale}`,
'tag_name'
]
}
);
nodes = get('streamTags.hits', data);
} else {
const data = await this.searchClient.queryForType(
'tag', query, generateUUID(), {
hitsPerPage: 100,
facetFilters: [
['tag_scope:SCOPE_ALL', 'tag_scope:SCOPE_CATEGORY']
],
restrictSearchableAttributes: [
`localizations.${locale}`,
'tag_name'
]
}
);
nodes = get('tags.hits', data);
}
const nodes = get('streamTags.hits', data);
if ( ! Array.isArray(nodes) )
return [];
const out = [], seen = new Set;
for(const node of nodes) {
if ( ! node || seen.has(node.tag_id) )
const tag_id = node.tag_id || node.objectID;
if ( ! node || seen.has(tag_id) )
continue;
seen.add(node.tag_id);
if ( ! this.tag_cache.has(node.tag_id) ) {
seen.add(tag_id);
if ( ! this.tag_cache.has(tag_id) ) {
const match = node.tag_name && LANGUAGE_MATCHER.exec(node.tag_name),
lang = match && match[1] || null;
const tag = {
id: node.tag_id,
value: node.tag_id,
is_language: node.tag_name && LANGUAGE_MATCHER.test(node.tag_name),
id: tag_id,
value: tag_id,
is_language: lang != null,
language: lang,
label: node.localizations && (node.localizations[locale] || node.localizations['en-us']) || node.tag_name
};
this.tag_cache.set(tag.id);
if ( node.description_localizations ) {
const desc = node.description_localizations[locale] || node.description_localizations['en-us'];
if ( desc )
tag.description = desc;
}
this.tag_cache.set(tag.id, tag);
out.push(tag);
} else {
out.push(this.tag_cache.get(node.tag_id));
const tag = this.tag_cache.get(tag_id);
if ( ! tag.description && node.description_localizations ) {
const desc = node.description_localizations[locale] || node.description_localizations['en-us'];
if ( desc ) {
tag.description = desc;
this.tag_cache.set(tag.id, tag);
}
}
out.push(tag);
}
}