From 7a01f5d79e4ec041b5c55e873981505b2d64cf7e Mon Sep 17 00:00:00 2001 From: Lordmau5 Date: Sun, 8 Apr 2018 19:44:44 +0200 Subject: [PATCH] Featured Follow buttons (#406) * Basic featured follow support * Implement `do_authorize` * More work on Featured Follows. - Refresh button - Also implement emotes * Remove debug log * Remove featured emote support in favor of next merge * Fix linting errors * Implement requested changes * Fixes --- .../modules/featured-follow.vue | 77 +++++++ .../modules/featured_follow.js | 213 ++++++++++++++++++ .../modules/featured_follow_follow.gql | 11 + .../modules/featured_follow_query.gql | 14 ++ .../modules/featured_follow_unfollow.gql | 9 + .../styles/featured_follow.scss | 34 +++ src/sites/twitch-twilight/styles/main.scss | 3 +- src/socket.js | 9 +- 8 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 src/sites/twitch-twilight/modules/featured-follow.vue create mode 100644 src/sites/twitch-twilight/modules/featured_follow.js create mode 100644 src/sites/twitch-twilight/modules/featured_follow_follow.gql create mode 100644 src/sites/twitch-twilight/modules/featured_follow_query.gql create mode 100644 src/sites/twitch-twilight/modules/featured_follow_unfollow.gql create mode 100644 src/sites/twitch-twilight/styles/featured_follow.scss diff --git a/src/sites/twitch-twilight/modules/featured-follow.vue b/src/sites/twitch-twilight/modules/featured-follow.vue new file mode 100644 index 00000000..3e574acc --- /dev/null +++ b/src/sites/twitch-twilight/modules/featured-follow.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/sites/twitch-twilight/modules/featured_follow.js b/src/sites/twitch-twilight/modules/featured_follow.js new file mode 100644 index 00000000..dd2cd103 --- /dev/null +++ b/src/sites/twitch-twilight/modules/featured_follow.js @@ -0,0 +1,213 @@ +'use strict'; + +// ============================================================================ +// Featured Follow +// ============================================================================ + +import Module from 'utilities/module'; +import {createElement as e} from 'utilities/dom'; + +import FEATURED_QUERY from './featured_follow_query.gql'; + +import FEATURED_FOLLOW from './featured_follow_follow.gql'; +import FEATURED_UNFOLLOW from './featured_follow_unfollow.gql'; + +const TWITCH_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([A-Za-z0-9_]+)/i; + +export default class FeaturedFollow extends Module { + constructor(...args) { + super(...args); + + this.should_enable = true; + + this.inject('site'); + this.inject('site.fine'); + this.inject('site.apollo'); + this.inject('i18n'); + this.inject('metadata'); + this.inject('settings'); + this.inject('socket'); + this.inject('site.router'); + + this.inject('chat'); + + this.settings.add('metadata.featured-follow', { + default: true, + + ui: { + path: 'Channel > Metadata >> Player', + title: 'Featured Follow', + description: 'Show a featured follow button with the currently featured users.', + component: 'setting-check-box' + }, + + changed: () => { + this.metadata.updateMetadata('following'); + } + }); + + this.metadata.definitions.following = { + order: 150, + button: true, + + popup: async (data, tip) => { + const vue = this.resolve('vue'), + _featured_follow_vue = import(/* webpackChunkName: "featured-follow" */ './featured-follow.vue'), + _follows = this.getFollowsForLogin(data.channel.login); + + const [, featured_follows_vue, follows] = await Promise.all([vue.enable(), _featured_follow_vue, _follows]); + + this._featured_follow_tip = tip; + tip.element.classList.remove('tw-pd-1'); + tip.element.classList.add('tw-balloon--lg'); + vue.component('featured-follow', featured_follows_vue.default); + return this.buildFeaturedFollowMenu(vue, data.channel.login, follows); + }, + + label: data => { + if (!this.settings.get('metadata.featured-follow') || !data || !data.channel || !data.channel.login) + return null; + + const follows = this.follow_data[data.channel.login]; + if (!follows || !Object.keys(follows).length) { + if (!this.vueFeaturedFollow || !this.vueFeaturedFollow.data.hasUpdate) { + return ''; + } + } + + return this.i18n.t('metadata.featured-follow.button.featured', 'Featured'); + }, + + icon: 'ffz-i-heart' + }; + + this.follow_data = {}; + + this.socket.on(':command:follow_buttons', data => { + for(const channel_login in data) { + if (!data.hasOwnProperty(channel_login)) continue; + + this.follow_data[channel_login] = data[channel_login]; + } + + if (this.vueFeaturedFollow) { + this.vueFeaturedFollow.data.hasUpdate = true; + } + + this.metadata.updateMetadata('following'); + }); + + // ffz.resolve('site.featured_follow').updateFeaturedChannels({ login: 'lordmau5' }, ['sirstendec','jugachi']); + } + + async getFollowsForLogin(login) { + const follow_data = this.follow_data && this.follow_data[login]; + if (!follow_data || follow_data.length === 0) return []; + + const ap_data = await this.apollo.client.query({ query: FEATURED_QUERY, variables: { logins: follow_data }}); + + const follows = {}; + for (const user of ap_data.data.users) { + follows[user.id] = { + id: user.id, + login: user.login, + displayName: user.displayName, + avatar: user.profileImageURL, + following: user.self.follower.followedAt != null, + disableNotifications: user.self.follower.disableNotifications + }; + } + return follows; + } + + buildFeaturedFollowMenu(vue, login, follows) { + const vueEl = new vue.Vue({ + el: e('div'), + render: h => this.vueFeaturedFollow = h('featured-follow', { + login, + follows, + hasUpdate: false, + + followUser: id => this.followUser(follows, id), + unfollowUser: id => this.unfollowUser(follows, id), + updateNotificationStatus: (id, oldStatus) => this.updateNotificationStatus(follows, id, oldStatus), + refresh: async () => { + if (!this.vueFeaturedFollow.data.hasUpdate) return; + + this.vueFeaturedFollow.data.follows = await this.getFollowsForLogin(login); + this.vueFeaturedFollow.data.hasUpdate = false; + this._featured_follow_tip.update(); + + if (this.vueFeaturedFollow.data.follows.length === 0) this.metadata.updateMetadata('following'); + }, + route: channel => this.route(channel) + }), + }); + + return vueEl.$el; + } + + updateFeaturedChannels(room, args) { + args = args.join(' ').trim().toLowerCase().split(/[ ,]+/); + + const out = []; + + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + const match = arg.match(TWITCH_URL); + + if (match) + arg = match[1]; + + if (arg !== '' && out.indexOf(arg) === -1) + out.push(arg); + } + + this.socket.call('update_follow_buttons', room.login, out) + .then(() => { + // this.log.info('Success!', data); + }) + .catch(() => 'There was an error communicating with the server.'); + // , (success, data) => { + // if (!success) { + // this.log.warn('Not a Success: ', data); + // // f.room_message(room, data); + // return; + // } + + // this.log.info('Success!', data); + + // // this.room.message(`The following buttons have been ${data ? 'updated' : 'disabled'}.`); + // }) ) + + } + + async followUser(follows, id) { + const ap_data = await this.apollo.client.mutate({ mutation: FEATURED_FOLLOW, variables: { targetID: id, disableNotifications: false }}); + + const follow = ap_data.data.followUser.follow; + + follows[id].following = follow.followedAt != null; + follows[id].disableNotifications = follow.disableNotifications; + } + + async updateNotificationStatus(follows, id, oldStatus) { + const ap_data = await this.apollo.client.mutate({ mutation: FEATURED_FOLLOW, variables: { targetID: id, disableNotifications: !oldStatus }}); + + const follow = ap_data.data.followUser.follow; + + follows[id].following = follow.followedAt != null; + follows[id].disableNotifications = follow.disableNotifications; + } + + async unfollowUser(follows, id) { + await this.apollo.client.mutate({ mutation: FEATURED_UNFOLLOW, variables: { targetID: id }}); + + follows[id].following = false; + follows[id].disableNotifications = false; + } + + route(channel) { + this.router.navigate('user', { userName: channel }); + } +} diff --git a/src/sites/twitch-twilight/modules/featured_follow_follow.gql b/src/sites/twitch-twilight/modules/featured_follow_follow.gql new file mode 100644 index 00000000..730f726c --- /dev/null +++ b/src/sites/twitch-twilight/modules/featured_follow_follow.gql @@ -0,0 +1,11 @@ +mutation($targetID: ID!, $disableNotifications: Boolean!) { + followUser(input: { + disableNotifications: $disableNotifications, + targetID: $targetID + }) { + follow { + disableNotifications + followedAt + } + } +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/featured_follow_query.gql b/src/sites/twitch-twilight/modules/featured_follow_query.gql new file mode 100644 index 00000000..a5781648 --- /dev/null +++ b/src/sites/twitch-twilight/modules/featured_follow_query.gql @@ -0,0 +1,14 @@ +query($logins: [String!]) { + users(logins: $logins) { + id + login + displayName + profileImageURL(width: 70) + self { + follower { + disableNotifications + followedAt + } + } + } +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/featured_follow_unfollow.gql b/src/sites/twitch-twilight/modules/featured_follow_unfollow.gql new file mode 100644 index 00000000..fc0e34a5 --- /dev/null +++ b/src/sites/twitch-twilight/modules/featured_follow_unfollow.gql @@ -0,0 +1,9 @@ +mutation($targetID: ID!) { + unfollowUser(input: { + targetID: $targetID + }) { + follow { + followedAt + } + } +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/styles/featured_follow.scss b/src/sites/twitch-twilight/styles/featured_follow.scss new file mode 100644 index 00000000..a616c056 --- /dev/null +++ b/src/sites/twitch-twilight/styles/featured_follow.scss @@ -0,0 +1,34 @@ +.ffz-featured-follow { + .scrollable-area { + max-height: 50vh; + } + + .ffz--featured-button-unfollow { + &:hover &:before { + content: '\e813'; + } + + > * { + pointer-events: none; + } + } + + .ffz--featured-button-notification { + > * { + pointer-events: none; + } + } + + .ffz--featured-follow-update { + animation: ffz--pulse-update 1s infinite ease-in-out alternate; + } + + > header { + padding: .9rem 1rem .9rem 2rem; + } +} + +@keyframes ffz--pulse-update { + from { box-shadow: none; } + to { box-shadow: 0 0 10px 0 var(--ffz-color-5); } +} diff --git a/src/sites/twitch-twilight/styles/main.scss b/src/sites/twitch-twilight/styles/main.scss index 9d5ce81e..8bfcb811 100644 --- a/src/sites/twitch-twilight/styles/main.scss +++ b/src/sites/twitch-twilight/styles/main.scss @@ -10,4 +10,5 @@ @import 'fixes'; -@import 'host_options'; \ No newline at end of file +@import 'host_options'; +@import 'featured_follow'; \ No newline at end of file diff --git a/src/socket.js b/src/socket.js index 16155c63..e68f546e 100644 --- a/src/socket.js +++ b/src/socket.js @@ -70,7 +70,14 @@ export default class SocketClient extends Module { this.on(':command:reconnect', this.reconnect, this); this.on(':command:do_authorize', challenge => { - this.log.warn('Unimplemented: do_authorize', challenge); + // this.log.warn('Unimplemented: do_authorize', challenge); + // We don't have our own IRC connection yet, so the site's chat has to do. + + const _chat = this.resolve('site.chat'); + const chat = _chat && _chat.currentChat; + const con = chat.chatService && chat.chatService.client && chat.chatService.client.connection; + + if (con && con.send) con.send(`PRIVMSG #frankerfacezauthorizer :AUTH ${challenge}`); });