mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-09-16 01:56:55 +00:00
Basic mod cards support (#423)
* More work on mod cards * More work on mod cards! Some sort of dynamic component thingy madoohickey * Change up implementation of tabs * Implement focus and tabindex * Remove unused GQL queries / mutations * Implement user info * Only show use rlogin if an international name was detected Also attempt to fix line height * Remove testing memes * Remove derps... whoops
This commit is contained in:
parent
c548f15290
commit
9ef7c2aee3
10 changed files with 411 additions and 3 deletions
|
@ -17,6 +17,7 @@ import ChatLine from './line';
|
||||||
import SettingsMenu from './settings_menu';
|
import SettingsMenu from './settings_menu';
|
||||||
import EmoteMenu from './emote_menu';
|
import EmoteMenu from './emote_menu';
|
||||||
import TabCompletion from './tab_completion';
|
import TabCompletion from './tab_completion';
|
||||||
|
import ModCards from './mod_cards';
|
||||||
|
|
||||||
|
|
||||||
const MESSAGE_TYPES = ((e = {}) => {
|
const MESSAGE_TYPES = ((e = {}) => {
|
||||||
|
@ -120,6 +121,8 @@ export default class ChatHook extends Module {
|
||||||
this.inject(EmoteMenu);
|
this.inject(EmoteMenu);
|
||||||
this.inject(TabCompletion);
|
this.inject(TabCompletion);
|
||||||
|
|
||||||
|
this.inject(ModCards);
|
||||||
|
|
||||||
|
|
||||||
this.ChatController = this.fine.define(
|
this.ChatController = this.fine.define(
|
||||||
'chat-controller',
|
'chat-controller',
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default class ChatLine extends Module {
|
||||||
this.inject('site');
|
this.inject('site');
|
||||||
this.inject('site.fine');
|
this.inject('site.fine');
|
||||||
this.inject('site.web_munch');
|
this.inject('site.web_munch');
|
||||||
|
this.inject('site.apollo');
|
||||||
this.inject(RichContent);
|
this.inject(RichContent);
|
||||||
|
|
||||||
this.inject('chat.actions');
|
this.inject('chat.actions');
|
||||||
|
@ -138,7 +139,7 @@ export default class ChatLine extends Module {
|
||||||
e('a', {
|
e('a', {
|
||||||
className: 'chat-author__display-name notranslate',
|
className: 'chat-author__display-name notranslate',
|
||||||
style: { color },
|
style: { color },
|
||||||
onClick: this.usernameClickHandler
|
onClick: t.parent.mod_cards.openCustomModCard.bind(t.parent.mod_cards, this, user)
|
||||||
}, [
|
}, [
|
||||||
user.userDisplayName,
|
user.userDisplayName,
|
||||||
user.isIntl && e('span', {
|
user.isIntl && e('span', {
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<section class="tw-background-c tw-relative">
|
||||||
|
<div class="tw-c-background tw-full-width tw-flex tw-flex-row tw-pd-r-05 tw-pd-l-1 tw-pd-y-1">
|
||||||
|
<div class="tw-mg-r-05">
|
||||||
|
<div class="tw-inline-block">
|
||||||
|
<button class="tw-button">
|
||||||
|
<span class="tw-button__text" data-a-target="tw-button-text">Add Friend</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-mg-r-05">
|
||||||
|
<div class="tw-inline-block">
|
||||||
|
<button class="tw-button" data-a-target="usercard-whisper-button" data-test-selector="whisper-button">
|
||||||
|
<span class="tw-button__text" data-a-target="tw-button-text">Whisper</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex-grow-1 tw-align-right">
|
||||||
|
<div class="tw-inline-block">
|
||||||
|
<button data-title="More Options" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-ellipsis-vert" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-c-background-alt-2 tw-pd-x-1 tw-pd-y-05">
|
||||||
|
<div>
|
||||||
|
<div class="tw-inline-block tw-pd-r-1">
|
||||||
|
<button data-title="Ban User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-block" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tw-inline-block tw-pd-r-1">
|
||||||
|
<button data-title="Timeout User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-clock" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tw-inline-block tw-pd-r-1">
|
||||||
|
<button data-title="Mod User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-star" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TabMixin from '../tab-mixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [TabMixin],
|
||||||
|
props: ['tab', 'user', 'room', 'currentUser']
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,25 @@
|
||||||
|
query($userLogin: String) {
|
||||||
|
user(login: $userLogin) {
|
||||||
|
bannerImageURL
|
||||||
|
displayName
|
||||||
|
id
|
||||||
|
login
|
||||||
|
profileImageURL(width: 50)
|
||||||
|
createdAt
|
||||||
|
followers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
profileViewCount
|
||||||
|
self {
|
||||||
|
friendship {
|
||||||
|
... on FriendEdge {
|
||||||
|
node {
|
||||||
|
displayName
|
||||||
|
id
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
src/sites/twitch-twilight/modules/chat/mod_cards/index.js
Normal file
117
src/sites/twitch-twilight/modules/chat/mod_cards/index.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Mod Cards Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import Module from 'utilities/module';
|
||||||
|
|
||||||
|
import {createElement} from 'utilities/dom';
|
||||||
|
|
||||||
|
import GET_USER_INFO from './get_user_info.gql';
|
||||||
|
|
||||||
|
export default class ModCards extends Module {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('site.apollo');
|
||||||
|
this.inject('i18n');
|
||||||
|
|
||||||
|
this.lastZIndex = 9001;
|
||||||
|
this.open_mod_cards = {};
|
||||||
|
this.tabs = {};
|
||||||
|
|
||||||
|
this.addTab('main', {
|
||||||
|
visible: () => true,
|
||||||
|
|
||||||
|
label: 'Main',
|
||||||
|
pill: 0,
|
||||||
|
|
||||||
|
data: (user, room) => ({
|
||||||
|
|
||||||
|
}),
|
||||||
|
component: () => import('./components/main.vue')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addTab(key, data) {
|
||||||
|
if (this.tabs[key]) return;
|
||||||
|
|
||||||
|
this.tabs[key] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openCustomModCard(t, user, e) {
|
||||||
|
// Old mod-card
|
||||||
|
// t.usernameClickHandler(e);
|
||||||
|
|
||||||
|
const posX = Math.min(window.innerWidth - 300, e.clientX),
|
||||||
|
posY = Math.min(window.innerHeight - 300, e.clientY),
|
||||||
|
room = {
|
||||||
|
id: t.props.channelID,
|
||||||
|
login: t.props.message.roomLogin
|
||||||
|
},
|
||||||
|
currentUser = {
|
||||||
|
isModerator: t.props.isCurrentUserModerator,
|
||||||
|
isStaff: t.props.isCurrentUserStaff
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.open_mod_cards[user.userLogin]) {
|
||||||
|
this.open_mod_cards[user.userLogin].style.zIndex = ++this.lastZIndex;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vue = this.resolve('vue'),
|
||||||
|
_mod_card_vue = import(/* webpackChunkName: "mod-card" */ './mod-card.vue'),
|
||||||
|
_user_info = this.apollo.client.query({
|
||||||
|
query: GET_USER_INFO,
|
||||||
|
variables: {
|
||||||
|
userLogin: user.userLogin
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [, mod_card_vue, user_info] = await Promise.all([vue.enable(), _mod_card_vue, _user_info]);
|
||||||
|
|
||||||
|
vue.component('mod-card', mod_card_vue.default);
|
||||||
|
|
||||||
|
const mod_card = this.open_mod_cards[user.userLogin] = this.buildModCard(vue, user_info.data.user, room, currentUser);
|
||||||
|
|
||||||
|
const main = document.querySelector('.twilight-root>.tw-full-height');
|
||||||
|
main.appendChild(mod_card);
|
||||||
|
|
||||||
|
mod_card.style.left = `${posX}px`;
|
||||||
|
mod_card.style.top = `${posY}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildModCard(vue, user, room, currentUser) {
|
||||||
|
this.log.info(user);
|
||||||
|
const vueEl = new vue.Vue({
|
||||||
|
el: createElement('div'),
|
||||||
|
render: h => {
|
||||||
|
const vueModCard = h('mod-card', {
|
||||||
|
activeTab: Object.keys(this.tabs)[0],
|
||||||
|
tabs: this.tabs,
|
||||||
|
user,
|
||||||
|
room,
|
||||||
|
currentUser,
|
||||||
|
|
||||||
|
rawUserAge: this.i18n.toLocaleString(new Date(user.createdAt)),
|
||||||
|
userAge: this.i18n.toHumanTime((new Date() - new Date(user.createdAt)) / 1000),
|
||||||
|
|
||||||
|
setActiveTab: tab => vueModCard.data.activeTab = tab,
|
||||||
|
|
||||||
|
focus: el => {
|
||||||
|
el.style.zIndex = ++this.lastZIndex;
|
||||||
|
},
|
||||||
|
|
||||||
|
close: () => {
|
||||||
|
this.open_mod_cards[user.login].remove();
|
||||||
|
this.open_mod_cards[user.login] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return vueModCard;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return vueEl.$el;
|
||||||
|
}
|
||||||
|
}
|
139
src/sites/twitch-twilight/modules/chat/mod_cards/mod-card.vue
Normal file
139
src/sites/twitch-twilight/modules/chat/mod_cards/mod-card.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ffz-mod-card tw-elevation-3 tw-c-background-alt tw-c-text tw-border tw-flex tw-flex-nowrap tw-flex-column"
|
||||||
|
tabindex="-1"
|
||||||
|
@focusin="doFocus"
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
:style="`background-image: url('${user.bannerImageURL}');`"
|
||||||
|
class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-relative"
|
||||||
|
>
|
||||||
|
<div class="tw-full-width tw-align-items-center tw-flex tw-flex-nowrap tw-pd-1 ffz--background-dimmer">
|
||||||
|
<div class="tw-inline-block">
|
||||||
|
<figure class="tw-avatar tw-avatar--size-50">
|
||||||
|
<div class="tw-overflow-hidden ">
|
||||||
|
<img
|
||||||
|
:src="user.profileImageURL"
|
||||||
|
class="tw-image"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="tw-ellipsis tw-inline-block">
|
||||||
|
<div class="tw-align-items-center tw-mg-l-1 ffz--info-lines">
|
||||||
|
<h4>
|
||||||
|
<a :href="`/${user.login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
|
||||||
|
{{ user.displayName }}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h5
|
||||||
|
v-if="user.displayName && user.displayName.toLowerCase() !== user.login.toLowerCase()"
|
||||||
|
>
|
||||||
|
<a :href="`/${user.login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
|
||||||
|
{{ user.login }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<span class="tw-mg-r-05">
|
||||||
|
<figure class="ffz-i-info tw-inline"/>
|
||||||
|
{{ user.profileViewCount }}
|
||||||
|
</span>
|
||||||
|
<span class="tw-mg-r-05">
|
||||||
|
<figure class="ffz-i-heart tw-inline"/>
|
||||||
|
{{ user.followers.totalCount }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
:data-title="rawUserAge"
|
||||||
|
data-tooltip-type="html"
|
||||||
|
class="ffz-tooltip"
|
||||||
|
>
|
||||||
|
<figure class="ffz-i-clock tw-inline"/>
|
||||||
|
{{ userAge }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex-grow-1 tw-pd-x-2"/>
|
||||||
|
<div class="tw-inline-block">
|
||||||
|
<button class="tw-button-icon tw-absolute tw-right-0 tw-top-0 tw-mg-t-05 tw-mg-r-05" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-cancel" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="tw-button-icon tw-absolute tw-right-0 tw-bottom-0 tw-mg-b-05 tw-mg-r-05" @click="close">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-ignore" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section class="tw-background-c">
|
||||||
|
<div class="mod-cards__tabs-container tw-border-t">
|
||||||
|
<div
|
||||||
|
v-for="(data, key) in tabs"
|
||||||
|
:key="key"
|
||||||
|
:id="`mod-cards__${key}`"
|
||||||
|
:class="{active: activeTab === key}"
|
||||||
|
class="mod-cards__tab tw-pd-x-1"
|
||||||
|
@click="setActiveTab(key)"
|
||||||
|
>
|
||||||
|
<span>{{ data.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<component
|
||||||
|
v-for="(tab, key) in tabs"
|
||||||
|
v-if="tab.visible && activeTab === key"
|
||||||
|
:is="tab.component"
|
||||||
|
:tab="tab"
|
||||||
|
:user="user"
|
||||||
|
:room="room"
|
||||||
|
:current-user="currentUser"
|
||||||
|
:key="key"
|
||||||
|
|
||||||
|
@close="close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import displace from 'displacejs';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return this.$vnode.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.createDrag();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.destroyDrag();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
destroyDrag() {
|
||||||
|
if ( this.displace ) {
|
||||||
|
this.displace.destroy();
|
||||||
|
this.displace = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createDrag() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.displace = displace(this.$el, {
|
||||||
|
handle: this.$el.querySelector('header'),
|
||||||
|
highlightInputs: true,
|
||||||
|
constrain: true
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
doFocus() {
|
||||||
|
this.focus(this.$el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
data() {
|
||||||
|
const data = this.tab.data;
|
||||||
|
if ( typeof data === 'function' )
|
||||||
|
return data.call(this, this.user, this.room, this.currentUser);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,3 +12,4 @@
|
||||||
|
|
||||||
@import 'host_options';
|
@import 'host_options';
|
||||||
@import 'featured_follow';
|
@import 'featured_follow';
|
||||||
|
@import 'mod_card';
|
40
src/sites/twitch-twilight/styles/mod_card.scss
Normal file
40
src/sites/twitch-twilight/styles/mod_card.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.ffz-mod-card {
|
||||||
|
width: 340px;
|
||||||
|
z-index: 9001;
|
||||||
|
|
||||||
|
> header {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz-tooltip {
|
||||||
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--background-dimmer {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--info-lines > * {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-cards__tabs-container {
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
> .mod-cards__tab {
|
||||||
|
position: relative;
|
||||||
|
top: -.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 3rem;
|
||||||
|
margin-right: .5rem;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
border-top: 1px solid #6441a4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue