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 EmoteMenu from './emote_menu';
|
||||
import TabCompletion from './tab_completion';
|
||||
import ModCards from './mod_cards';
|
||||
|
||||
|
||||
const MESSAGE_TYPES = ((e = {}) => {
|
||||
|
@ -120,6 +121,8 @@ export default class ChatHook extends Module {
|
|||
this.inject(EmoteMenu);
|
||||
this.inject(TabCompletion);
|
||||
|
||||
this.inject(ModCards);
|
||||
|
||||
|
||||
this.ChatController = this.fine.define(
|
||||
'chat-controller',
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class ChatLine extends Module {
|
|||
this.inject('site');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.web_munch');
|
||||
this.inject('site.apollo');
|
||||
this.inject(RichContent);
|
||||
|
||||
this.inject('chat.actions');
|
||||
|
@ -138,7 +139,7 @@ export default class ChatLine extends Module {
|
|||
e('a', {
|
||||
className: 'chat-author__display-name notranslate',
|
||||
style: { color },
|
||||
onClick: this.usernameClickHandler
|
||||
onClick: t.parent.mod_cards.openCustomModCard.bind(t.parent.mod_cards, this, user)
|
||||
}, [
|
||||
user.userDisplayName,
|
||||
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 '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