diff --git a/src/sites/twitch-twilight/modules/directory/browse_popular.js b/src/sites/twitch-twilight/modules/directory/browse_popular.js new file mode 100644 index 00000000..3204029b --- /dev/null +++ b/src/sites/twitch-twilight/modules/directory/browse_popular.js @@ -0,0 +1,104 @@ +'use strict'; + +// ============================================================================ +// Directory (Following, for now) +// ============================================================================ + +import {SiteModule} from 'utilities/module'; +import {get} from 'utilities/object'; + +export default class BrowsePopular extends SiteModule { + constructor(...args) { + super(...args); + + this.inject('site.apollo'); + this.inject('site.fine'); + this.inject('settings'); + + this.apollo.registerModifier('BrowsePage_Popular', `query { + streams { + edges { + node { + createdAt + broadcaster { + profileImageURL(width: 70) + } + } + } + } + }`); + + this.ChannelCard = this.fine.define( + 'browse-all-channel-card', + n => n.props && n.props.channelName && n.props.linkTo && n.props.linkTo.state && n.props.linkTo.state.medium === 'twitch_browse_directory' + ); + + this.apollo.registerModifier('BrowsePage_Popular', res => this.modifyStreams(res), false); + } + + onEnable() { + this.ChannelCard.ready((cls, instances) => { + // Popular Directory Channel Cards + this.apollo.ensureQuery( + 'BrowsePage_Popular', + 'data.streams.edges.node.0.createdAt' + ); + + for(const inst of instances) this.updateChannelCard(inst); + }); + + this.ChannelCard.on('update', this.updateChannelCard, this); + this.ChannelCard.on('mount', this.updateChannelCard, this); + this.ChannelCard.on('unmount', this.parent.clearUptime, this); + } + + modifyStreams(res) { // eslint-disable-line class-methods-use-this + const blockedGames = this.settings.provider.get('directory.game.blocked-games') || []; + + const newStreams = []; + + const edges = get('data.streams.edges', res); + if (!edges) return res; + + for (let i = 0; i < edges.length; i++) { + const edge = edges[i]; + const node = edge.node; + + const s = node.viewersCount = new Number(node.viewersCount || 0); + s.profileImageURL = node.broadcaster.profileImageURL; + s.createdAt = node.createdAt; + s.login = node.broadcaster.login; + s.displayName = node.broadcaster.displayName; + + if (!node.game || node.game && !blockedGames.includes(node.game.name)) newStreams.push(edge); + } + res.data.streams.edges = newStreams; + return res; + } + + updateChannelCard(inst) { + const container = this.fine.getHostNode(inst); + if (!container) return; + + if (container.classList.contains('ffz-modified-channel-card')) return; + container.classList.add('ffz-modified-channel-card'); + + this.parent.updateUptime(inst, 'props.viewerCount.createdAt', '.tw-card-img'); + this.parent.addCardAvatar(inst, 'props.viewerCount', '.tw-card'); + + const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || []; + const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg'; + + if (inst.props.type === 'watch_party') + container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts')); + + const img = container.querySelector && container.querySelector('.tw-card-img img'); + if (img == null) return; + + if (hiddenThumbnails.includes(inst.props.gameTitle)) { + img.src = hiddenPreview; + } else { + img.src = inst.props.imageSrc; + } + } +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/directory/following.js b/src/sites/twitch-twilight/modules/directory/following.js index 858c6674..36ef947a 100644 --- a/src/sites/twitch-twilight/modules/directory/following.js +++ b/src/sites/twitch-twilight/modules/directory/following.js @@ -86,10 +86,12 @@ export default class Following extends SiteModule { this.apollo.registerModifier('FollowingLive_CurrentUser', `query { currentUser { followedLiveUsers { - nodes { - profileImageURL(width: 70) - stream { - createdAt + edges { + node { + profileImageURL(width: 70) + stream { + createdAt + } } } } @@ -155,20 +157,28 @@ export default class Following extends SiteModule { const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || []; const blockedGames = this.settings.provider.get('directory.game.blocked-games') || []; - const newLiveNodes = []; + const newStreams = []; - const nodes = res.data.currentUser.followedLiveUsers.nodes; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + const followedLiveUsers = get('data.currentUser.followedLiveUsers', res); + if (!followedLiveUsers) + return res; + + const oldMode = !!followedLiveUsers.nodes; + const edgesOrNodes = followedLiveUsers.nodes || followedLiveUsers.edges; + + for (let i = 0; i < edgesOrNodes.length; i++) { + const edge = edgesOrNodes[i]; + const node = edge.node || edge; const s = node.stream.viewersCount = new Number(node.stream.viewersCount || 0); s.profileImageURL = node.profileImageURL; s.createdAt = node.stream.createdAt; if (node.stream.game && hiddenThumbnails.includes(node.stream.game.name)) node.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg'; - if (!node.stream.game || node.stream.game && !blockedGames.includes(node.stream.game.name)) newLiveNodes.push(node); + if (!node.stream.game || node.stream.game && !blockedGames.includes(node.stream.game.name)) newStreams.push(edge); } - res.data.currentUser.followedLiveUsers.nodes = newLiveNodes; + res.data.currentUser.followedLiveUsers[oldMode ? 'nodes' : 'edges'] = newStreams; + return res; } @@ -176,12 +186,19 @@ export default class Following extends SiteModule { const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || []; const blockedGames = this.settings.provider.get('directory.game.blocked-games') || []; - const nodes = res.data.currentUser.followedHosts.nodes; + const followedHosts = get('data.currentUser.followedHosts', res); + if (!followedHosts) + return res; + this.hosts = {}; const newHostNodes = []; - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; + const oldMode = !!followedHosts.nodes; + const edgesOrNodes = followedHosts.nodes || followedHosts.edges; + + for (let i = 0; i < edgesOrNodes.length; i++) { + const edge = edgesOrNodes[i]; + const node = edge.node || edge; const s = node.hosting.stream.viewersCount = new Number(node.hosting.stream.viewersCount || 0); s.profileImageURL = node.hosting.profileImageURL; @@ -194,7 +211,7 @@ export default class Following extends SiteModule { channels: [node.displayName] }; if (node.hosting.stream.game && hiddenThumbnails.includes(node.hosting.stream.game.name)) node.hosting.stream.previewImageURL = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg'; - if (!node.hosting.stream.game || node.hosting.stream.game && !blockedGames.includes(node.hosting.stream.game.name)) newHostNodes.push(node); + if (!node.hosting.stream.game || node.hosting.stream.game && !blockedGames.includes(node.hosting.stream.game.name)) newHostNodes.push(edge); } else { this.hosts[node.hosting.displayName].nodes.push(node); this.hosts[node.hosting.displayName].channels.push(node.displayName); @@ -202,7 +219,7 @@ export default class Following extends SiteModule { } if (this.settings.get('directory.following.group-hosts')) { - res.data.currentUser.followedHosts.nodes = newHostNodes; + res.data.currentUser.followedHosts[oldMode ? 'nodes' : 'edges'] = newHostNodes; } return res; } @@ -220,12 +237,14 @@ export default class Following extends SiteModule { n => get('data.currentUser.followedLiveUsers.nodes.0.profileImageURL', n) !== undefined || + get('data.currentUser.followedLiveUsers.edges.0.node.profileImageURL', n) !== undefined + || get('data.currentUser.followedHosts.nodes.0.hosting.profileImageURL', n) !== undefined ); } else if (this.router.match[1] === 'live') { this.apollo.ensureQuery( 'FollowingLive_CurrentUser', - 'data.currentUser.followedLiveUsers.nodes.0.profileImageURL' + 'data.currentUser.followedLiveUsers.nodes.0.profileImageURL' || 'data.currentUser.followedLiveUsers.edges.0.node.profileImageURL' ); } else if (this.router.match[1] === 'hosts') { this.apollo.ensureQuery( @@ -353,9 +372,10 @@ export default class Following extends SiteModule { ) ); - (document.body.querySelector('.twilight-root') || document.body).appendChild(this.hostMenu); + const root = (document.body.querySelector('.twilight-root') || document.body); + root.appendChild(this.hostMenu); - this.hostMenuPopper = new Popper(document.body, this.hostMenu, { + this.hostMenuPopper = new Popper(root, this.hostMenu, { placement: 'bottom-start', modifiers: { flip: { diff --git a/src/sites/twitch-twilight/modules/directory/index.js b/src/sites/twitch-twilight/modules/directory/index.js index 86ee08f5..132af9ba 100644 --- a/src/sites/twitch-twilight/modules/directory/index.js +++ b/src/sites/twitch-twilight/modules/directory/index.js @@ -12,6 +12,7 @@ import {get} from 'utilities/object'; import Following from './following'; import Game from './game'; import Community from './community'; +import BrowsePopular from './browse_popular'; export default class Directory extends SiteModule { constructor(...args) { @@ -30,6 +31,7 @@ export default class Directory extends SiteModule { this.inject(Following); this.inject(Game); this.inject(Community); + this.inject(BrowsePopular); this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false); @@ -134,6 +136,7 @@ export default class Directory extends SiteModule { this.css_tweaks.toggleHide('boxart-hover', boxart === 1); this.ChannelCard.ready((cls, instances) => { + // Game Directory Channel Cards this.apollo.ensureQuery( 'GamePage_Game', 'data.directory.streams.edges.0.node.createdAt' @@ -149,37 +152,40 @@ export default class Directory extends SiteModule { updateChannelCard(inst) { - const uptimeSel = inst.props.directoryType === 'GAMES' ? '.tw-thumbnail-card .tw-card-img' : '.tw-card .tw-aspect > div'; - const avatarSel = inst.props.directoryType === 'GAMES' ? '.tw-thumbnail-card' : '.tw-card'; + const container = this.fine.getHostNode(inst); + if (!container) return; - this.updateUptime(inst, 'props.streamNode.viewersCount.createdAt', uptimeSel); - this.addCardAvatar(inst, avatarSel); + this.updateUptime(inst, 'props.streamNode.viewersCount.createdAt', '.tw-card-img'); + this.addCardAvatar(inst, 'props.streamNode.viewersCount', '.tw-card'); - const type = inst.props.directoryType; + const type = get('props.directoryType', inst); const hiddenThumbnails = this.settings.provider.get('directory.game.hidden-thumbnails') || []; const hiddenPreview = 'https://static-cdn.jtvnw.net/ttv-static/404_preview-320x180.jpg'; - const container = this.fine.getHostNode(inst); - - if (inst.props.streamNode.type === 'watch_party') + if (get('props.streamNode.type', inst) === 'watch_party' || get('props.type', inst) === 'watch_party') container.classList.toggle('tw-hide', this.settings.get('directory.hide-vodcasts')); - const img = container && container.querySelector && container.querySelector(`${uptimeSel} img`); - if (img === null) return; + const img = container.querySelector && container.querySelector('.tw-card-img img'); + if (img == null) return; - if (type === 'GAMES' && hiddenThumbnails.includes(inst.props.directoryName) || - type === 'COMMUNITIES' && hiddenThumbnails.includes(inst.props.streamNode.game.name)) { + if (type === 'GAMES' && hiddenThumbnails.includes(get('props.directoryName', inst)) || + type === 'COMMUNITIES' && hiddenThumbnails.includes(get('props.streamNode.game.name', inst))) { img.src = hiddenPreview; } else { - img.src = inst.props.streamNode.previewImageURL; + img.src = get('props.streamNode.previewImageURL', inst) || get('props.imageSrc', inst); } } modifyStreams(res) { // eslint-disable-line class-methods-use-this + const blockedGames = this.settings.provider.get('directory.game.blocked-games') || []; + const gamePage = get('data.directory.__typename', res) === 'Game'; + const newStreams = []; - const edges = res.data.directory.streams.edges; + const edges = get('data.directory.streams.edges', res); + if (!edges) return res; + for (let i = 0; i < edges.length; i++) { const edge = edges[i]; const node = edge.node; @@ -187,8 +193,10 @@ export default class Directory extends SiteModule { const s = node.viewersCount = new Number(node.viewersCount || 0); s.profileImageURL = node.broadcaster.profileImageURL; s.createdAt = node.createdAt; + s.login = node.broadcaster.login; + s.displayName = node.broadcaster.displayName; - newStreams.push(edge); + if (gamePage || (!node.game || node.game && !blockedGames.includes(node.game.name))) newStreams.push(edge); } res.data.directory.streams.edges = newStreams; return res; @@ -256,10 +264,14 @@ export default class Directory extends SiteModule { } - addCardAvatar(inst, selector) { + addCardAvatar(inst, created_path, selector) { const container = this.fine.getHostNode(inst), card = container && container.querySelector && container.querySelector(selector), - setting = this.settings.get('directory.show-channel-avatars'); + setting = this.settings.get('directory.show-channel-avatars'), + data = get(created_path, inst); + + if ( ! card ) + return; // Remove old elements const hiddenBodyCard = card.querySelector('.tw-card-body.tw-hide'); @@ -274,10 +286,10 @@ export default class Directory extends SiteModule { if (channelAvatar !== null) channelAvatar.remove(); - if ( ! card || setting === 0 ) + if ( setting === 0 ) return; - if (inst.props.streamNode.viewersCount.profileImageURL) { + if (data) { if (setting === 1) { const cardDiv = card.querySelector('.tw-card-body'); const modifiedDiv = e('div', { @@ -286,11 +298,11 @@ export default class Directory extends SiteModule { const avatarDiv = e('a', { className: 'ffz-channel-avatar tw-mg-r-05 tw-mg-t-05', - href: `/${inst.props.streamNode.broadcaster.login}`, - onclick: event => this.hijackUserClick(event, inst.props.streamNode.broadcaster.login) + href: `/${data.login}`, + onclick: event => this.hijackUserClick(event, data.login) }, e('img', { - title: inst.props.streamNode.broadcaster.displayName, - src: inst.props.streamNode.viewersCount.profileImageURL + title: data.displayName, + src: data.profileImageURL })); const cardDivParent = cardDiv.parentElement; @@ -306,13 +318,13 @@ export default class Directory extends SiteModule { } else if (setting === 2 || setting === 3) { const avatarElement = e('a', { className: 'ffz-channel-avatar', - href: `/${inst.props.streamNode.broadcaster.login}`, - onclick: event => this.hijackUserClick(event, inst.props.streamNode.broadcaster.login) + href: `/${data.login}`, + onclick: event => this.hijackUserClick(event, data.login) }, e('div', 'live-channel-card__boxart tw-bottom-0 tw-absolute', e('figure', 'tw-aspect tw-aspect--align-top', e('img', { - title: inst.props.streamNode.broadcaster.displayName, - src: inst.props.streamNode.viewersCount.profileImageURL + title: data.displayName, + src: data.profileImageURL }) ) ));