mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-03 00:18:31 +00:00
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
This commit is contained in:
parent
4f1ebcdfbf
commit
7a01f5d79e
8 changed files with 368 additions and 2 deletions
77
src/sites/twitch-twilight/modules/featured-follow.vue
Normal file
77
src/sites/twitch-twilight/modules/featured-follow.vue
Normal file
|
@ -0,0 +1,77 @@
|
|||
<template lang="html">
|
||||
<div class="ffz-featured-follow tw-c-background">
|
||||
<header class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap">
|
||||
<h4>{{ t('metadata.featured-follow.title', 'Featured Follow') }}</h4>
|
||||
|
||||
<div class="tw-flex-grow-1 tw-pd-x-2"/>
|
||||
<button :class="{ 'ffz--featured-follow-update': hasUpdate, 'tw-button--disabled': !hasUpdate }" class="tw-button tw-button--hollow" @click="refresh">
|
||||
<span class="tw-button__icon tw-button__icon--left">
|
||||
<figure class="ffz-i-arrows-cw"/>
|
||||
</span>
|
||||
<span class="tw-button__text tw-pd-l-0">
|
||||
Refresh
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
<section class="tw-border-t tw-full-width tw-full-height">
|
||||
<main class="tw-flex-grow-1 scrollable-area" data-simplebar="init">
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
<div v-for="user in follows" :key="user.id" class="tw-border-t ffz--featured-user">
|
||||
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-t-1 tw-mg-b-1">
|
||||
<div class="ffz-channel-avatar">
|
||||
<a :href="'/' + user.login" :title="user.login" @click.prevent="route(user.login)"><img :src="user.avatar"></a>
|
||||
</div>
|
||||
<a :href="'/' + user.login" :title="user.login" @click.prevent="route(user.login)"><p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">{{ user.displayName }}</p></a>
|
||||
<div class="tw-flex-grow-1 tw-pd-x-2"/>
|
||||
|
||||
<button
|
||||
v-if="user.following"
|
||||
:data-title="`Unfollow ${user.login}`"
|
||||
data-tooltip-type="html"
|
||||
class="tw-button tw-button--status tw-button--success ffz-tooltip ffz--featured-button-unfollow"
|
||||
@click="unfollowUser(user.id)"
|
||||
>
|
||||
<span class="tw-button__icon tw-button__icon--status tw-flex">
|
||||
<figure class="ffz-i-heart ffz--featured-button-unfollow-button"/>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="user.following"
|
||||
:data-title="`${(user.disableNotifications ? 'Enable' : 'Disable')} Notifications`"
|
||||
data-tooltip-type="html"
|
||||
class="tw-button-icon tw-mg-l-05 ffz-tooltip ffz--featured-button-notification"
|
||||
@click="updateNotificationStatus(user.id, user.disableNotifications)"
|
||||
>
|
||||
<span class="tw-button__icon tw-flex">
|
||||
<figure :class="{ 'ffz-i-bell': !user.disableNotifications, 'ffz-i-bell-off': user.disableNotifications }"/>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="tw-button"
|
||||
@click="followUser(user.id)"
|
||||
>
|
||||
<span class="tw-button__icon tw-button__icon--left">
|
||||
<figure class="ffz-i-heart"/>
|
||||
</span>
|
||||
<span class="tw-button__text">
|
||||
{{ t('featured-follow.button.follow', 'Follow') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return this.$vnode.data;
|
||||
}
|
||||
}
|
||||
</script>
|
213
src/sites/twitch-twilight/modules/featured_follow.js
Normal file
213
src/sites/twitch-twilight/modules/featured_follow.js
Normal file
|
@ -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 });
|
||||
}
|
||||
}
|
11
src/sites/twitch-twilight/modules/featured_follow_follow.gql
Normal file
11
src/sites/twitch-twilight/modules/featured_follow_follow.gql
Normal file
|
@ -0,0 +1,11 @@
|
|||
mutation($targetID: ID!, $disableNotifications: Boolean!) {
|
||||
followUser(input: {
|
||||
disableNotifications: $disableNotifications,
|
||||
targetID: $targetID
|
||||
}) {
|
||||
follow {
|
||||
disableNotifications
|
||||
followedAt
|
||||
}
|
||||
}
|
||||
}
|
14
src/sites/twitch-twilight/modules/featured_follow_query.gql
Normal file
14
src/sites/twitch-twilight/modules/featured_follow_query.gql
Normal file
|
@ -0,0 +1,14 @@
|
|||
query($logins: [String!]) {
|
||||
users(logins: $logins) {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
profileImageURL(width: 70)
|
||||
self {
|
||||
follower {
|
||||
disableNotifications
|
||||
followedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation($targetID: ID!) {
|
||||
unfollowUser(input: {
|
||||
targetID: $targetID
|
||||
}) {
|
||||
follow {
|
||||
followedAt
|
||||
}
|
||||
}
|
||||
}
|
34
src/sites/twitch-twilight/styles/featured_follow.scss
Normal file
34
src/sites/twitch-twilight/styles/featured_follow.scss
Normal file
|
@ -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); }
|
||||
}
|
|
@ -10,4 +10,5 @@
|
|||
|
||||
@import 'fixes';
|
||||
|
||||
@import 'host_options';
|
||||
@import 'host_options';
|
||||
@import 'featured_follow';
|
|
@ -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}`);
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue