mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.0.0-rc1.5
Add an option to hide the mouse over the player. This doesn't work well in Chrome. Oh well, we tried. Add an option to not automatically join raids for that comfy falling asleep experience. Fix in-line actions not rendering when they should. Fix detokenizeMessage for changes to Twitch's message format. Don't try to preview the `create` clip URL. Start using functional components where possible for performance. Stop logging GraphQL errors to save our Sentry quota. Begin implementing chat rendering on top of Vue. For now, we've got functional components for every type of chat token. We've got a lot of work ahead of us. This will eventually be used for mod card history, chat panes, and maybe even pinned rooms. Add an event to clean orphan tooltips. Useful for when we click something we know will change DOM elements with active tooltips, like a Close button for example.
This commit is contained in:
parent
9ef7c2aee3
commit
9dc8252df0
58 changed files with 1037 additions and 392 deletions
|
@ -1,3 +1,17 @@
|
|||
<div class="list-header">4.0.0-rc1.5<span>@eb1433f63b4667bf9010</span> <time datetime="2018-05-10">(2018-05-10)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Option to hide the mouse cursor over the player. This does not work consistently in some web browser due to browser policies preventing the cursor from being hidden in certain situations.</li>
|
||||
<li>Added: Option to not automatically join raids.</li>
|
||||
<li>Fixed: In-line Actions not rendering when Is Moderator is set to False and the user is not a moderator.</li>
|
||||
<li>Fixed: In-line Actions not appearing correctly in the editor preview line.</li>
|
||||
<li>Fixed: Adapt to changes Twitch has made to pre-tokenized chat message data.</li>
|
||||
<li>Fixed: Do not try showing a rich clip preview for the bad link <code>https://clips.twitch.tv/create</code> that is commonly erroneously posted in chat.</li>
|
||||
<li>Fixed: Tooltips not closing in some situations.</li>
|
||||
<li>Fixed: Chat message types sometimes not being detected, causing messages with <code>/me</code> to not appear correctly.</li>
|
||||
<li>Changed: Improve emote menu performance using functional components.</li>
|
||||
<li>Changed: Stop logging GraphQL errors. Twitch's GraphQL endpoint breaks too often for this to be useful.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">4.0.0-rc1.4<span>@2009dc29d6bd5e122bd6</span> <time datetime="2018-04-29">(2018-04-29)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Fixed: Issue with mod actions not working properly on AutoMod messages due to a missing channel mapping.</li>
|
||||
|
|
Binary file not shown.
|
@ -86,6 +86,8 @@
|
|||
|
||||
<glyph glyph-name="gift" unicode="" d="M518 93v400h-179v-400q0-14 10-21t26-8h107q16 0 26 8t10 21z m-255 471h109l-70 90q-15 17-39 17-22 0-38-15t-15-38 15-38 38-16z m384 54q0 22-15 38t-38 15q-24 0-39-17l-69-90h108q22 0 38 16t15 38z m210-143v-179q0-7-5-12t-13-5h-53v-233q0-22-16-37t-38-16h-607q-22 0-38 16t-16 37v233h-53q-8 0-13 5t-5 12v179q0 8 5 13t13 5h245q-51 0-88 36t-37 89 37 88 88 37q60 0 94-43l72-92 71 92q34 43 94 43 52 0 88-37t37-88-37-89-88-36h245q8 0 13-5t5-13z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="views" unicode="" d="M688 38h-375l-250 250v62 63l250 250h375l250-250v-63-62l-250-250z m-188 500c-103 0-187-84-187-188 0-103 84-187 187-187 104 0 188 84 188 187 0 104-84 188-188 188z m0-250c-35 0-62 28-62 62s27 63 62 63 63-28 63-63-28-62-63-62z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="link-ext" unicode="" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="twitter" unicode="" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
|
||||
|
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc1.4',
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc1.5',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
|
|
6
src/modules/chat/actions/components/preview-icon.vue
Normal file
6
src/modules/chat/actions/components/preview-icon.vue
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template functional>
|
||||
<figure
|
||||
:class="`${props.data.icon||'ffz-i-zreknarf'}`"
|
||||
:style="{color:props.color}"
|
||||
/>
|
||||
</template>
|
5
src/modules/chat/actions/components/preview-image.vue
Normal file
5
src/modules/chat/actions/components/preview-image.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<figure class="mod-icon__image">
|
||||
<img :src="props.data.image">
|
||||
</figure>
|
||||
</template>
|
5
src/modules/chat/actions/components/preview-text.vue
Normal file
5
src/modules/chat/actions/components/preview-text.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<span :style="{color: props.color}">
|
||||
{{ props.data.text }}
|
||||
</span>
|
||||
</template>
|
|
@ -181,10 +181,10 @@ export default class Actions extends Module {
|
|||
def = this.renderers[ap.type];
|
||||
|
||||
if ( ! def || disp.disabled ||
|
||||
(disp.mod_icons != null && disp.mod_icons !== mod_icons) ||
|
||||
(disp.mod != null && disp.mod !== (current_user ? current_user.moderator : false)) ||
|
||||
(disp.staff != null && disp.staff !== (current_user ? current_user.staff : false)) ||
|
||||
(disp.deleted != null && disp.deleted !== msg.deleted) )
|
||||
(disp.mod_icons != null && disp.mod_icons !== !!mod_icons) ||
|
||||
(disp.mod != null && disp.mod !== (current_user ? !!current_user.moderator : false)) ||
|
||||
(disp.staff != null && disp.staff !== (current_user ? !!current_user.staff : false)) ||
|
||||
(disp.deleted != null && disp.deleted !== !!msg.deleted) )
|
||||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<template>
|
||||
<figure
|
||||
:class="`${data.icon||'ffz-i-zreknarf'}`"
|
||||
:style="{color}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<figure class="mod-icon__image">
|
||||
<img :src="data.image">
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<span :style="{color}">
|
||||
{{ data.text }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['data', 'color']
|
||||
}
|
||||
|
||||
</script>
|
|
@ -12,9 +12,10 @@ export const text = {
|
|||
title_i18n: 'setting.actions.appearance.text',
|
||||
|
||||
colored: true,
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-text.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-text.vue'),
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-text.vue'),
|
||||
|
||||
component: () => import(/* webpackChunkName: 'main-menu' */ './components/preview-text.vue'),
|
||||
render(data, createElement, color) {
|
||||
return <span style={{color}}>{data.text}</span>;
|
||||
}
|
||||
|
@ -30,8 +31,7 @@ export const icon = {
|
|||
title_i18n: 'setting.actions.appearance.icon',
|
||||
|
||||
colored: true,
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-icon.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-icon.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-icon.vue'),
|
||||
|
||||
load(data) {
|
||||
if ( data.icon && data.icon.startsWith('ffz-fa') )
|
||||
|
@ -40,6 +40,8 @@ export const icon = {
|
|||
return true;
|
||||
},
|
||||
|
||||
component: () => import(/* webpackChunkName: 'main-menu' */ './components/preview-icon.vue'),
|
||||
|
||||
render(data, createElement, color) {
|
||||
return <figure style={{color}} class={`${data.icon||'ffz-i-zreknarf'}`} />;
|
||||
}
|
||||
|
@ -54,9 +56,9 @@ export const image = {
|
|||
title: 'Image',
|
||||
title_i18n: 'setting.actions.appearance.image',
|
||||
|
||||
preview: () => import(/* webpackChunkName: 'main-menu' */ './preview-image.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-image.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-image.vue'),
|
||||
|
||||
component: () => import(/* webpackChunkName: 'main-menu' */ './components/preview-image.vue'),
|
||||
render(data, createElement) {
|
||||
return <figure class="mod-icon__image"><img src={data.image} /></figure>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export const open_url = {
|
|||
url: 'https://link.example/{{user.login}}'
|
||||
},
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-url.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-url.vue'),
|
||||
|
||||
title: 'Open URL',
|
||||
description: '%{options.url}',
|
||||
|
@ -71,7 +71,7 @@ export const chat = {
|
|||
title: 'Chat Command',
|
||||
description: '%{options.command}',
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-chat.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-chat.vue'),
|
||||
|
||||
process(data) {
|
||||
return transformPhrase(
|
||||
|
@ -121,7 +121,7 @@ export const ban = {
|
|||
}
|
||||
}],
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-ban.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-ban.vue'),
|
||||
|
||||
title: 'Ban User',
|
||||
|
||||
|
@ -152,7 +152,7 @@ export const timeout = {
|
|||
duration: 600
|
||||
},
|
||||
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './edit-timeout.vue'),
|
||||
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
|
||||
|
||||
title: 'Timeout User',
|
||||
description: '%{options.duration} second%{options.duration|en_plural}',
|
||||
|
|
11
src/modules/chat/components/chat-cheer.vue
Normal file
11
src/modules/chat/components/chat-cheer.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template functional>
|
||||
<span
|
||||
:data-prefix="props.token.prefix"
|
||||
:data-amount="props.token.amount"
|
||||
:data-tier="props.token.tier"
|
||||
:data-individuals="JSON.stringify(props.token.individuals || null)"
|
||||
:alt="props.token.text"
|
||||
class="ffz-cheer ffz-tooltip"
|
||||
data-tooltip-type="cheer"
|
||||
/>
|
||||
</template>
|
11
src/modules/chat/components/chat-line.vue
Normal file
11
src/modules/chat/components/chat-line.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
13
src/modules/chat/components/chat-link.vue
Normal file
13
src/modules/chat/components/chat-link.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template functional>
|
||||
<a
|
||||
:href="props.token.url"
|
||||
:data-url="props.token.url"
|
||||
:data-is-mail="props.token.is_mail"
|
||||
class="ffz-tooltip"
|
||||
data-tooltip-type="link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{ props.token.text }}
|
||||
</a>
|
||||
</template>
|
8
src/modules/chat/components/chat-mention.vue
Normal file
8
src/modules/chat/components/chat-mention.vue
Normal file
|
@ -0,0 +1,8 @@
|
|||
<template functional>
|
||||
<strong
|
||||
:class="{'ffz--mention-me': props.token.me}"
|
||||
class="chat-line__message-mention"
|
||||
>
|
||||
{{ props.token.text }}
|
||||
</strong>
|
||||
</template>
|
119
src/modules/chat/components/chat-rich.vue
Normal file
119
src/modules/chat/components/chat-rich.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<a :href="url" class="chat-card__link" target="_blank" rel="noreferrer noopener">
|
||||
<div class="ffz--chat-card tw-elevation-1 tw-mg-t">
|
||||
<div class="tw-c-background tw-flex tw-flex-nowrap tw-pd-05">
|
||||
<div class="chat-card__preview-img tw-c-background-alt-2 tw-align-items-center tw-flex tw-flex-shrink-0 tw-justify-content-center">
|
||||
<div class="tw-card-img tw-flex-shrink-0 tw-flex tw-justify-content-center">
|
||||
<img
|
||||
v-if="error"
|
||||
class="chat-card__error-img"
|
||||
src=""
|
||||
data-test-selector="chat-card-error"
|
||||
>
|
||||
<figure
|
||||
v-else
|
||||
class="tw-aspect tw-aspect--16x9 tw-aspect--align-top"
|
||||
>
|
||||
<img
|
||||
v-if="loaded && image"
|
||||
:src="image"
|
||||
:alt="title"
|
||||
class="tw-image"
|
||||
>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{'ffz--two-line': desc_2}"
|
||||
class="tw-overflow-hidden tw-align-items-center tw-flex"
|
||||
>
|
||||
<div class="tw-full-width tw-pd-l-1">
|
||||
<div class="chat-card__title tw-ellipsis">
|
||||
<span
|
||||
:title="title"
|
||||
class="tw-font-size-5"
|
||||
data-test-selector="chat-card-title"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-ellipsis">
|
||||
<span
|
||||
:title="desc_1"
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
data-test-selector="chat-card-description"
|
||||
>
|
||||
{{ desc_1 }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="desc_2" class="tw-ellipsis">
|
||||
<span
|
||||
:title="desc_2"
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
data-test-selector="chat-card-description"
|
||||
>
|
||||
{{ desc_2 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {timeout} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['data', 'url'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
error: false,
|
||||
title: null,
|
||||
desc_1: null,
|
||||
desc_2: null,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
let data;
|
||||
try {
|
||||
data = this.data.getData();
|
||||
if ( data instanceof Promise ) {
|
||||
const to_wait = this.data.timeout || 1000;
|
||||
if ( to_wait )
|
||||
data = await timeout(data, to_wait);
|
||||
else
|
||||
data = await data;
|
||||
}
|
||||
|
||||
if ( ! data )
|
||||
data = {
|
||||
error: true,
|
||||
title: this.t('card.error', 'An error occured.'),
|
||||
desc_1: this.t('card.empty', 'No data was returned.')
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
data = {
|
||||
error: true,
|
||||
title: this.t('card.error', 'An error occured.'),
|
||||
desc_1: String(err)
|
||||
}
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.error = data.error;
|
||||
this.image = data.image;
|
||||
this.title = data.title;
|
||||
this.desc_1 = data.desc_1;
|
||||
this.desc_2 = data.desc_2;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -27,7 +27,7 @@ export const Clips = {
|
|||
const match = CLIP_URL.exec(token.url),
|
||||
apollo = this.resolve('site.apollo');
|
||||
|
||||
if ( ! apollo || ! match )
|
||||
if ( ! apollo || ! match || match[1] === 'create' )
|
||||
return;
|
||||
|
||||
return {
|
||||
|
|
|
@ -16,13 +16,6 @@ export default class Room {
|
|||
constructor(manager, id, login) {
|
||||
this._destroy_timer = null;
|
||||
|
||||
this.manager = manager;
|
||||
this._id = id;
|
||||
this.login = login;
|
||||
|
||||
if ( id )
|
||||
this.manager.room_ids[id] = this;
|
||||
|
||||
this.refs = new Set;
|
||||
this.style = new ManagedStyle(`room--${login}`);
|
||||
|
||||
|
@ -30,6 +23,13 @@ export default class Room {
|
|||
this.users = {};
|
||||
this.user_ids = {};
|
||||
|
||||
this.manager = manager;
|
||||
this._id = id;
|
||||
this.login = login;
|
||||
|
||||
if ( id )
|
||||
this.manager.room_ids[id] = this;
|
||||
|
||||
this.manager.emit(':room-add', this);
|
||||
this.load_data();
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ export const Links = {
|
|||
type: 'link',
|
||||
priority: 50,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-link.vue'),
|
||||
|
||||
render(token, createElement) {
|
||||
return (<a
|
||||
class="ffz-tooltip"
|
||||
|
@ -193,6 +195,8 @@ export const Mentions = {
|
|||
type: 'mention',
|
||||
priority: 0,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-mention.vue'),
|
||||
|
||||
render(token, createElement) {
|
||||
return (<strong class={`chat-line__message-mention${token.me ? ' ffz--mention-me' : ''}`}>
|
||||
{token.text}
|
||||
|
@ -264,6 +268,8 @@ export const CheerEmotes = {
|
|||
type: 'cheer',
|
||||
priority: 40,
|
||||
|
||||
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-cheer.vue'),
|
||||
|
||||
render(token, createElement) {
|
||||
return (<span
|
||||
class="ffz-cheer ffz-tooltip"
|
||||
|
@ -432,10 +438,50 @@ export const CheerEmotes = {
|
|||
// Addon Emotes
|
||||
// ============================================================================
|
||||
|
||||
const render_emote = (token, createElement) => {
|
||||
const mods = token.modifiers || [], ml = mods.length,
|
||||
emote = createElement('img', {
|
||||
class: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`,
|
||||
attrs: {
|
||||
src: token.src,
|
||||
srcSet: token.srcSet,
|
||||
alt: token.text,
|
||||
'data-tooltip-type': 'emote',
|
||||
'data-provider': token.provider,
|
||||
'data-id': token.id,
|
||||
'data-set': token.set,
|
||||
'data-code': token.code,
|
||||
'data-variant': token.variant,
|
||||
'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null,
|
||||
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null
|
||||
}
|
||||
});
|
||||
|
||||
if ( ! ml )
|
||||
return emote;
|
||||
|
||||
return createElement('span', {
|
||||
class: `${EMOTE_CLASS} modified-emote`,
|
||||
attrs: {
|
||||
'data-provider': token.provider,
|
||||
'data-id': token.id,
|
||||
'data-set': token.set
|
||||
}
|
||||
}, [emote, mods.map(x => createElement('span', {key: x.text}, render_emote(x, createElement)))])
|
||||
}
|
||||
|
||||
|
||||
export const AddonEmotes = {
|
||||
type: 'emote',
|
||||
priority: 10,
|
||||
|
||||
component: {
|
||||
functional: true,
|
||||
render(createElement, {props}) {
|
||||
return render_emote(props.token, createElement);
|
||||
}
|
||||
},
|
||||
|
||||
render(token, createElement) {
|
||||
const mods = token.modifiers || [], ml = mods.length,
|
||||
emote = (<img
|
||||
|
|
|
@ -11,14 +11,14 @@ export default class User {
|
|||
this.manager = manager;
|
||||
this.room = room;
|
||||
|
||||
this.emote_sets = new SourcedSet;
|
||||
this.badges = new SourcedSet;
|
||||
|
||||
this._id = id;
|
||||
this.login = login;
|
||||
|
||||
if ( id )
|
||||
(room || manager).user_ids[id] = this;
|
||||
|
||||
this.emote_sets = new SourcedSet;
|
||||
this.badges = new SourcedSet;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
|
|
@ -178,6 +178,18 @@
|
|||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="deleting">
|
||||
<button class="tw-button tw-button--text" @click="$emit('remove', action)">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="deleting = false">
|
||||
<span class="tw-button__text ffz-i-cancel">
|
||||
{{ t('setting.cancel', 'Cancel') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canEdit"
|
||||
|
@ -188,7 +200,7 @@
|
|||
{{ t('setting.edit', 'Edit') }}
|
||||
</span>
|
||||
</button>
|
||||
<button class="tw-button tw-button--text" @click="$emit('remove', action)">
|
||||
<button class="tw-button tw-button--text" @click="deleting = true">
|
||||
<span class="tw-button__text ffz-i-trash">
|
||||
{{ t('setting.delete', 'Delete') }}
|
||||
</span>
|
||||
|
@ -207,6 +219,7 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
deleting: false,
|
||||
editing: false,
|
||||
edit_data: null
|
||||
}
|
||||
|
@ -292,9 +305,8 @@ export default {
|
|||
if ( ! this.display || ! this.display.appearance )
|
||||
return;
|
||||
|
||||
const disp = this.display.display, out = [];
|
||||
if ( ! disp )
|
||||
return this.t('setting.actions.visible.always', 'always');
|
||||
const disp = this.display.display || {},
|
||||
out = [];
|
||||
|
||||
if ( disp.disable )
|
||||
return this.t('setting.actions.visible.never', 'never');
|
||||
|
@ -323,6 +335,9 @@ export default {
|
|||
else if ( disp.deleted === false )
|
||||
out.push(this.t('setting.actions.visible.undeleted', 'if message not deleted'));
|
||||
|
||||
if ( ! out.length )
|
||||
return this.t('setting.actions.visible.always', 'always');
|
||||
|
||||
return out.join(', ');
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
class="ffz-mod-icon mod-icon tw-c-text-alt-2 tw-font-size-4"
|
||||
>
|
||||
<component
|
||||
v-if="renderer && renderer.preview"
|
||||
:is="renderer.preview"
|
||||
v-if="renderer && renderer.component"
|
||||
:is="renderer.component"
|
||||
:data="act.appearance"
|
||||
:color="color"
|
||||
/>
|
||||
|
|
|
@ -404,10 +404,10 @@ export default {
|
|||
if ( disp.mod != null && disp.mod !== this.is_moderator )
|
||||
return false;
|
||||
|
||||
if ( disp.mod_icons != null && disp.mod !== this.with_mod_icons )
|
||||
if ( disp.mod_icons != null && disp.mod_icons !== this.with_mod_icons )
|
||||
return false;
|
||||
|
||||
if ( disp.staff != null && disp.mod !== this.is_staff )
|
||||
if ( disp.staff != null && disp.staff !== this.is_staff )
|
||||
return false;
|
||||
|
||||
if ( disp.deleted != null && disp.deleted !== this.is_deleted )
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
// Tooltip Handling
|
||||
// ============================================================================
|
||||
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {createElement, sanitize} from 'utilities/dom';
|
||||
import {has, maybe_call} from 'utilities/object';
|
||||
|
||||
import Tooltip from 'utilities/tooltip';
|
||||
|
@ -31,6 +31,7 @@ export default class TooltipProvider extends Module {
|
|||
]
|
||||
}
|
||||
|
||||
this.types.text = target => sanitize(target.dataset.title);
|
||||
this.types.html = target => target.dataset.title;
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,12 @@ export default class TooltipProvider extends Module {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.on(':cleanup', this.cleanup);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.tips.cleanup();
|
||||
}
|
||||
|
||||
checkDelayShow(target, tip) {
|
||||
|
@ -89,7 +96,7 @@ export default class TooltipProvider extends Module {
|
|||
}
|
||||
|
||||
process(target, tip) {
|
||||
const type = target.dataset.tooltipType,
|
||||
const type = target.dataset.tooltipType || 'text',
|
||||
handler = this.types[type];
|
||||
|
||||
if ( ! handler )
|
||||
|
|
|
@ -156,6 +156,8 @@ Twilight.ROUTES = {
|
|||
'dir-game-index': '/directory/game/:gameName',
|
||||
'dir-all': '/directory/all/:filter?',
|
||||
'dir-category': '/directory/:category?',
|
||||
'dash': '/:userName/dashboard',
|
||||
'dash-automod': '/:userName/dashboard/settings/automod',
|
||||
'event': '/event/:eventName',
|
||||
'popout': '/popout/:userName/chat',
|
||||
'video': '/videos/:videoID',
|
||||
|
|
|
@ -16,6 +16,8 @@ export default class Channel extends Module {
|
|||
this.inject('settings');
|
||||
this.inject('site.fine');
|
||||
|
||||
this.left_raids = new Set;
|
||||
|
||||
this.settings.add('channel.hosting.enable', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -26,16 +28,52 @@ export default class Channel extends Module {
|
|||
changed: val => this.updateChannelHosting(val)
|
||||
});
|
||||
|
||||
|
||||
this.settings.add('channel.raids.no-autojoin', {
|
||||
default: false,
|
||||
ui: {
|
||||
path: 'Channel > Behavior >> Raids',
|
||||
title: 'Do not automatically join raids.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.ChannelPage = this.fine.define(
|
||||
'channel-page',
|
||||
n => n.handleHostingChange,
|
||||
['user']
|
||||
);
|
||||
|
||||
this.RaidController = this.fine.define(
|
||||
'raid-controller',
|
||||
n => n.handleLeaveRaid && n.handleJoinRaid,
|
||||
['user']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
this.ChannelPage.on('mount', this.wrapChannelPage, this);
|
||||
this.RaidController.on('mount', this.noAutoRaids, this);
|
||||
this.RaidController.on('update', this.noAutoRaids, this);
|
||||
|
||||
this.RaidController.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
this.noAutoRaids(inst);
|
||||
});
|
||||
|
||||
this.ChannelPage.on('update', inst => {
|
||||
if ( this.settings.get('channel.hosting.enable') || ! inst.state.isHosting )
|
||||
return;
|
||||
|
||||
// We can't do this immediately because the player state
|
||||
// occasionally screws up if we do.
|
||||
setTimeout(() => {
|
||||
inst.ffzExpectedHost = inst.state.videoPlayerSource;
|
||||
inst.ffzOldHostHandler(null);
|
||||
});
|
||||
});
|
||||
|
||||
this.ChannelPage.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
|
@ -44,6 +82,22 @@ export default class Channel extends Module {
|
|||
}
|
||||
|
||||
|
||||
noAutoRaids(inst) {
|
||||
if ( this.settings.get('channel.raids.no-autojoin') )
|
||||
setTimeout(() => {
|
||||
if ( inst.state.raid && inst.hasJoinedCurrentRaid ) {
|
||||
const id = inst.state.raid.id;
|
||||
if ( this.left_raids.has(id) )
|
||||
return;
|
||||
|
||||
this.log.info('Automatically leaving raid:', id);
|
||||
this.left_raids.add(id);
|
||||
inst.handleLeaveRaid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
wrapChannelPage(inst) {
|
||||
if ( inst._ffz_hosting_wrapped )
|
||||
return;
|
||||
|
|
|
@ -193,8 +193,8 @@ export default class EmoteMenu extends Module {
|
|||
|
||||
|
||||
this.MenuWrapper = this.fine.wrap('ffz-emote-menu');
|
||||
this.MenuSection = this.fine.wrap('ffz-menu-section');
|
||||
this.MenuEmote = this.fine.wrap('ffz-menu-emote');
|
||||
//this.MenuSection = this.fine.wrap('ffz-menu-section');
|
||||
//this.MenuEmote = this.fine.wrap('ffz-menu-emote');
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
|
@ -281,25 +281,14 @@ export default class EmoteMenu extends Module {
|
|||
React = this.web_munch.getModule('react'),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
this.MenuEmote = class FFZMenuEmote extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
this.MenuEmote = function({source, data, lock, locked, all_locked, onClickEmote}) {
|
||||
const handle_click = e => {
|
||||
if ( ! t.emotes.handleClick(e) )
|
||||
onClickEmote(data.name);
|
||||
};
|
||||
|
||||
handleClick(event) {
|
||||
if ( ! t.emotes.handleClick(event) )
|
||||
this.props.onClickEmote(this.props.data.name);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data,
|
||||
lock = this.props.lock,
|
||||
locked = this.props.locked,
|
||||
favorite = data.favorite,
|
||||
|
||||
sellout = lock ?
|
||||
this.props.all_locked ?
|
||||
const sellout = lock ?
|
||||
all_locked ?
|
||||
t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) :
|
||||
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock)
|
||||
: null;
|
||||
|
@ -312,12 +301,12 @@ export default class EmoteMenu extends Module {
|
|||
data-set={data.set_id}
|
||||
data-code={data.code}
|
||||
data-variant={data.variant}
|
||||
data-no-source={this.props.source}
|
||||
data-no-source={source}
|
||||
data-name={data.name}
|
||||
aria-label={data.name}
|
||||
data-locked={data.locked}
|
||||
data-sellout={sellout}
|
||||
onClick={!data.locked && this.handleClick}
|
||||
onClick={!data.locked && handle_click}
|
||||
>
|
||||
<figure class="emote-picker__emote-figure">
|
||||
<img
|
||||
|
@ -329,13 +318,10 @@ export default class EmoteMenu extends Module {
|
|||
width={data.width ? `${data.width}px` : null}
|
||||
/>
|
||||
</figure>
|
||||
{favorite && (<figure class="ffz--favorite ffz-i-star" />)}
|
||||
{data.favorite && (<figure class="ffz--favorite ffz-i-star" />)}
|
||||
{locked && (<figure class="ffz-i-lock" />)}
|
||||
</button>);
|
||||
</button>)
|
||||
}
|
||||
}
|
||||
|
||||
this.fine.wrap('ffz-menu-emote', this.MenuEmote);
|
||||
|
||||
|
||||
this.MenuSection = class FFZMenuSection extends React.Component {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
// Chat Hooks
|
||||
// ============================================================================
|
||||
|
||||
import {ColorAdjuster, Color} from 'utilities/color';
|
||||
import {ColorAdjuster} from 'utilities/color';
|
||||
import {setChildren} from 'utilities/dom';
|
||||
import {has, split_chars} from 'utilities/object';
|
||||
|
||||
|
@ -334,13 +334,12 @@ export default class ChatHook extends Module {
|
|||
|
||||
grabTypes() {
|
||||
const ct = this.web_munch.getModule('chat-types');
|
||||
if ( ct ) {
|
||||
|
||||
this.automod_types = ct && ct.a || AUTOMOD_TYPES;
|
||||
this.chat_types = ct && ct.b || CHAT_TYPES;
|
||||
this.message_types = ct && ct.c || MESSAGE_TYPES;
|
||||
this.mod_types = ct && ct.e || MOD_TYPES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onEnable() {
|
||||
|
|
|
@ -28,6 +28,8 @@ export default class ChatLine extends Module {
|
|||
this.inject('site.apollo');
|
||||
this.inject(RichContent);
|
||||
|
||||
this.inject('site.chat.mod_cards');
|
||||
|
||||
this.inject('chat.actions');
|
||||
|
||||
this.ChatLine = this.fine.define(
|
||||
|
@ -127,6 +129,9 @@ export default class ChatLine extends Module {
|
|||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r),
|
||||
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
|
||||
|
||||
if ( ! this.ffz_user_click_handler )
|
||||
this.ffz_user_click_handler = event => event.ctrlKey ? this.usernameClickHandler(event) : t.mod_cards.openCard(r, user, event);
|
||||
|
||||
let cls = 'chat-line__message',
|
||||
out = (tokens.length || ! msg.ffz_type) ? [
|
||||
this.props.showTimestamps && e('span', {
|
||||
|
@ -139,7 +144,7 @@ export default class ChatLine extends Module {
|
|||
e('a', {
|
||||
className: 'chat-author__display-name notranslate',
|
||||
style: { color },
|
||||
onClick: t.parent.mod_cards.openCustomModCard.bind(t.parent.mod_cards, this, user)
|
||||
onClick: this.usernameClickHandler, // this.ffz_user_click_handler
|
||||
}, [
|
||||
user.userDisplayName,
|
||||
user.isIntl && e('span', {
|
||||
|
@ -274,25 +279,26 @@ export function detokenizeMessage(msg) {
|
|||
|
||||
for(let i=0; i < l; i++) {
|
||||
const part = parts[i],
|
||||
type = part.type,
|
||||
content = part.content;
|
||||
|
||||
if ( type === 0 )
|
||||
if ( ! content )
|
||||
continue;
|
||||
|
||||
if ( typeof content === 'string' )
|
||||
ret = content;
|
||||
|
||||
else if ( type === 1 )
|
||||
else if ( content.recipient )
|
||||
ret = `@${content.recipient}`;
|
||||
|
||||
else if ( type === 2 )
|
||||
ret = content.displayText;
|
||||
else if ( content.url )
|
||||
ret = content.url;
|
||||
|
||||
else if ( type === 3 ) {
|
||||
if ( content.cheerAmount ) {
|
||||
else if ( content.cheerAmount )
|
||||
ret = `${content.alt}${content.cheerAmount}`;
|
||||
|
||||
} else {
|
||||
const url = (content.images.themed ? content.images.dark : content.images.sources)['1x'],
|
||||
match = /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url),
|
||||
else if ( content.images ) {
|
||||
const url = (content.images.themed ? content.images.dark : content.images.sources),
|
||||
match = url && /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url['1x']),
|
||||
id = match && match[1];
|
||||
|
||||
ret = content.alt;
|
||||
|
@ -300,20 +306,20 @@ export function detokenizeMessage(msg) {
|
|||
if ( id ) {
|
||||
const em = emotes[id] = emotes[id] || [],
|
||||
offset = last_type > 0 ? 1 : 0;
|
||||
|
||||
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
|
||||
}
|
||||
}
|
||||
|
||||
if ( last_type > 0 )
|
||||
ret = ` ${ret}`;
|
||||
|
||||
} else if ( type === 4 )
|
||||
ret = `https://clips.twitch.tv/${content.slug}`;
|
||||
} else
|
||||
continue;
|
||||
|
||||
if ( ret ) {
|
||||
idx += ret.length;
|
||||
last_type = type;
|
||||
out.push(ret);
|
||||
last_type = part.type;
|
||||
out.push(ret)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
265
src/sites/twitch-twilight/modules/chat/mod_cards/card.vue
Normal file
265
src/sites/twitch-twilight/modules/chat/mod_cards/card.vue
Normal file
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<div
|
||||
:style="{zIndex: z}"
|
||||
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="0"
|
||||
@focusin="onFocus"
|
||||
@keyup.esc="close"
|
||||
>
|
||||
<header
|
||||
:style="loaded && `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">
|
||||
<figure class="tw-avatar tw-avatar--size-50">
|
||||
<div v-if="loaded" class="tw-overflow-hidden ">
|
||||
<img
|
||||
:src="user.profileImageURL"
|
||||
class="tw-image"
|
||||
>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="tw-ellipsis tw-inline-block">
|
||||
<div class="tw-align-items-center tw-mg-l-1 ffz--info-lines">
|
||||
<h4>
|
||||
<a :href="`/${login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
|
||||
{{ displayName }}
|
||||
</a>
|
||||
</h4>
|
||||
<h5
|
||||
v-if="displayName && displayName.toLowerCase() !== login"
|
||||
>
|
||||
<a :href="`/${login}`" class="tw-link tw-link--hover-underline-none tw-link--inherit" target="_blank">
|
||||
{{ login }}
|
||||
</a>
|
||||
</h5>
|
||||
<div v-if="loaded">
|
||||
<span
|
||||
:data-title="t('viewer-card.views', 'Views')"
|
||||
class="ffz-tooltip tw-mg-r-05 ffz-i-views"
|
||||
>
|
||||
{{ t(null, '%{views|number}', {views: user.profileViewCount}) }}
|
||||
</span>
|
||||
<span
|
||||
:data-title="t('viewer-card.followers', 'Followers')"
|
||||
class="ffz-tooltip tw-mg-r-05 ffz-i-heart"
|
||||
>
|
||||
{{ t(null, '%{followers|number}', {followers: user.followers.totalCount}) }}
|
||||
</span>
|
||||
<span
|
||||
data-title="rawUserAge"
|
||||
class="ffz-tooltip ffz-i-clock"
|
||||
>
|
||||
{ userAge }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex-grow-1 tw-pd-x-2" />
|
||||
<button
|
||||
:data-title="t('viewer-card.close', 'Close')"
|
||||
class="ffz-tooltip 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
|
||||
v-show="! pinned"
|
||||
:data-title="t('viewer-card.pin', 'Pin')"
|
||||
class="ffz-tooltip tw-button-icon tw-absolute tw-right-0 tw-bottom-0 tw-mg-b-05 tw-mg-r-05"
|
||||
@click="pin"
|
||||
>
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-pin" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<error-tab v-if="errored" />
|
||||
<template v-else-if="loaded">
|
||||
<section class="tw-c-background">
|
||||
<div class="mod-cards__tabs-container tw-border-t">
|
||||
<div
|
||||
v-for="(data, key) in tabs"
|
||||
:key="key"
|
||||
:id="`mod-cards__${key}`"
|
||||
:class="{active: active_tab === key}"
|
||||
class="mod-cards__tab tw-pd-x-1"
|
||||
@click="active_tab = key"
|
||||
>
|
||||
<span>{{ data.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="current_tab.component"
|
||||
:tab="current_tab"
|
||||
:channel="channel"
|
||||
:user="user"
|
||||
:self="self"
|
||||
:getFFZ="getFFZ"
|
||||
@close="close"
|
||||
/>
|
||||
</keep-alive>
|
||||
</template>
|
||||
<loading-tab v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LoadingTab from './components/loading-tab.vue';
|
||||
import ErrorTab from './components/error-tab.vue';
|
||||
|
||||
import displace from 'displacejs';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'error-tab': ErrorTab,
|
||||
'loading-tab': LoadingTab
|
||||
},
|
||||
|
||||
props: ['tabs', 'room', 'raw_user', 'pos_x', 'pos_y', 'data', 'getZ', 'getFFZ'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
active_tab: Object.keys(this.tabs)[0],
|
||||
z: this.getZ(),
|
||||
|
||||
loaded: false,
|
||||
errored: false,
|
||||
pinned: false,
|
||||
|
||||
user: null,
|
||||
channel: null,
|
||||
self: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
login() {
|
||||
if ( this.loaded )
|
||||
return this.user.login;
|
||||
|
||||
return this.raw_user.login;
|
||||
},
|
||||
|
||||
displayName() {
|
||||
if ( this.loaded )
|
||||
return this.user.displayName;
|
||||
|
||||
return this.raw_user.displayName || this.raw_user.login;
|
||||
},
|
||||
|
||||
current_tab() {
|
||||
return this.tabs[this.active_tab];
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount() {
|
||||
this.data.then(data => {
|
||||
this.loaded = true;
|
||||
this.user = data.data.targetUser;
|
||||
this.channel = data.data.channelUser;
|
||||
this.self = data.data.currentUser;
|
||||
|
||||
}).catch(err => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
this.errored = true;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this._on_resize = this.handleResize.bind(this);
|
||||
window.addEventListener('resize', this._on_resize);
|
||||
this.createDrag();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.destroyDrag();
|
||||
|
||||
if ( this._on_resize ) {
|
||||
window.removeEventListener('resize', this._on_resize);
|
||||
this._on_resize = null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
constrain() {
|
||||
const el = this.$el;
|
||||
let parent = el.parentElement,
|
||||
moved = false;
|
||||
|
||||
if ( ! parent )
|
||||
parent = document.body;
|
||||
|
||||
const box = el.getBoundingClientRect(),
|
||||
pbox = parent.getBoundingClientRect();
|
||||
|
||||
if ( box.top < pbox.top ) {
|
||||
el.style.top = `${el.offsetTop + (pbox.top - box.top)}px`;
|
||||
moved = true;
|
||||
} else if ( box.bottom > pbox.bottom ) {
|
||||
el.style.top = `${el.offsetTop - (box.bottom - pbox.bottom)}px`;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if ( box.left < pbox.left ) {
|
||||
el.style.left = `${el.offsetLeft + (pbox.left - box.left)}px`;
|
||||
moved = true;
|
||||
} else if ( box.right > pbox.right ) {
|
||||
el.style.left = `${el.offsetLeft - (box.right - pbox.right)}px`;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if ( moved && this.displace )
|
||||
this.displace.reinit();
|
||||
},
|
||||
|
||||
pin() {
|
||||
this.pinned = true;
|
||||
this.$emit('pin');
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
createDrag() {
|
||||
this.$nextTick(() => {
|
||||
this.displace = displace(this.$el, {
|
||||
handle: this.$el.querySelector('header'),
|
||||
highlightInputs: true,
|
||||
constrain: true
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
destroyDrag() {
|
||||
if ( this.displace ) {
|
||||
this.displace.destroy();
|
||||
this.displace = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if ( this.displace )
|
||||
this.displace.reinit()
|
||||
},
|
||||
|
||||
onFocus() {
|
||||
console.log('got focus!');
|
||||
|
||||
this.z = this.getZ();
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,11 @@
|
|||
<template functional>
|
||||
<div class="tw-align-center tw-pd-1">
|
||||
<div class="tw-mg-t-1 tw-mg-b-2">
|
||||
<img
|
||||
src="//cdn.frankerfacez.com/emoticon/26608/2"
|
||||
srcSet="//cdn.frankerfacez.com/emoticon/26608/2 1x, //cdn.frankerfacez.com/emoticon/26608/4 2x"
|
||||
>
|
||||
</div>
|
||||
{{ parent.t('viewer-card.error', 'There was an error loading data for this user.') }}
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<div class="tw-align-center tw-pd-1">
|
||||
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
|
||||
</div>
|
||||
</template>
|
|
@ -1,6 +1,6 @@
|
|||
<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-background-c 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">
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div>
|
||||
<error-tab v-if="errored" />
|
||||
<loading-tab v-else-if="loading" />
|
||||
<template v-else>
|
||||
{{ JSON.stringify(data) }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import LoadingTab from './loading-tab.vue';
|
||||
import ErrorTab from './error-tab.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'loading-tab': LoadingTab,
|
||||
'error-tab': ErrorTab
|
||||
},
|
||||
|
||||
props: ['tab', 'channel', 'user', 'self', 'getFFZ'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
errored: false,
|
||||
|
||||
data: null
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const socket = this.getFFZ().resolve('socket');
|
||||
if ( ! socket ) {
|
||||
this.errored = true;
|
||||
return;
|
||||
}
|
||||
|
||||
socket.call('get_name_history', this.user.login).then(data => {
|
||||
this.loading = false;
|
||||
this.data = data;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.errored = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -1,25 +1,50 @@
|
|||
query($userLogin: String) {
|
||||
user(login: $userLogin) {
|
||||
bannerImageURL
|
||||
displayName
|
||||
query FFZ_ViewerCard($targetLogin: String!, $channelID: ID!) {
|
||||
targetUser: user(login: $targetLogin) {
|
||||
id
|
||||
login
|
||||
profileImageURL(width: 50)
|
||||
displayName
|
||||
bannerImageURL
|
||||
profileImageURL(width: 70)
|
||||
createdAt
|
||||
profileViewCount
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
profileViewCount
|
||||
self {
|
||||
friendship {
|
||||
... on FriendEdge {
|
||||
node {
|
||||
displayName
|
||||
...friendButtonFragment
|
||||
}
|
||||
channelUser: user(id: $channelID) {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
subscriptionProducts {
|
||||
id
|
||||
price
|
||||
url
|
||||
emoteSetID
|
||||
emotes {
|
||||
id
|
||||
}
|
||||
}
|
||||
self {
|
||||
isModerator
|
||||
}
|
||||
}
|
||||
currentUser {
|
||||
id
|
||||
login
|
||||
roles {
|
||||
isSiteAdmin
|
||||
isStaff
|
||||
isGlobalMod
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment friendButtonFragment on User {
|
||||
id
|
||||
self {
|
||||
friendship {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import Module from 'utilities/module';
|
||||
|
||||
import {has} from 'utilities/object';
|
||||
import {createElement} from 'utilities/dom';
|
||||
|
||||
import GET_USER_INFO from './get_user_info.gql';
|
||||
|
@ -14,104 +15,162 @@ export default class ModCards extends Module {
|
|||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('site.apollo');
|
||||
this.inject('i18n');
|
||||
this.inject('settings');
|
||||
this.inject('site.apollo');
|
||||
|
||||
this.lastZIndex = 9001;
|
||||
this.open_mod_cards = {};
|
||||
this.tabs = {};
|
||||
|
||||
this.last_z = 9000;
|
||||
this.open_cards = {};
|
||||
this.unpinned_card = null;
|
||||
|
||||
this.addTab('main', {
|
||||
visible: () => true,
|
||||
visible: true,
|
||||
|
||||
label: 'Main',
|
||||
pill: 0,
|
||||
pill: 3,
|
||||
|
||||
data: (user, room) => ({
|
||||
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/main.vue')
|
||||
});
|
||||
|
||||
}),
|
||||
component: () => import('./components/main.vue')
|
||||
this.addTab('stats', {
|
||||
label: 'Stats',
|
||||
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/stats.vue')
|
||||
});
|
||||
|
||||
this.addTab('name-history', {
|
||||
label: 'Name History',
|
||||
component: () => import(/* webpackChunkName: 'viewer-cards' */ './components/name-history.vue')
|
||||
});
|
||||
}
|
||||
|
||||
addTab(key, data) {
|
||||
if (this.tabs[key]) return;
|
||||
if ( this.tabs[key] )
|
||||
return this.log.warn(`Attempted to re-define known tab "${key}"`);
|
||||
|
||||
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
|
||||
onEnable() {
|
||||
this.vue = this.resolve('vue');
|
||||
}
|
||||
|
||||
|
||||
async loadVue() {
|
||||
if ( this._vue_loaded )
|
||||
return;
|
||||
|
||||
const [, card_component] = await Promise.all([
|
||||
this.vue.enable(),
|
||||
import(/* webpackChunkName: 'viewer-cards' */ './card.vue')
|
||||
]);
|
||||
|
||||
this.vue.component('viewer-card', card_component.default);
|
||||
this._vue_loaded = true;
|
||||
}
|
||||
|
||||
|
||||
async openCard(room, user, event) {
|
||||
if ( user.userLogin && ! user.login )
|
||||
user = {
|
||||
login: user.userLogin,
|
||||
id: user.userID,
|
||||
displayName: user.userDisplayName,
|
||||
};
|
||||
|
||||
if (this.open_mod_cards[user.userLogin]) {
|
||||
this.open_mod_cards[user.userLogin].style.zIndex = ++this.lastZIndex;
|
||||
const old_card = this.open_cards[user.login];
|
||||
if ( old_card ) {
|
||||
old_card.$el.style.zIndex = ++this.last_z;
|
||||
old_card.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const vue = this.resolve('vue'),
|
||||
_mod_card_vue = import(/* webpackChunkName: "mod-card" */ './mod-card.vue'),
|
||||
_user_info = this.apollo.client.query({
|
||||
let pos_x = event ? event.clientX : window.innerWidth / 2,
|
||||
pos_y = event ? event.clientY + 15 : window.innerHeight / 2;
|
||||
|
||||
if ( this.unpinned_card ) {
|
||||
const card = this.unpinned_card;
|
||||
|
||||
pos_x = card.$el.offsetLeft;
|
||||
pos_y = card.$el.offsetTop;
|
||||
|
||||
card.close();
|
||||
}
|
||||
|
||||
// We start this first...
|
||||
const user_info = this.apollo.client.query({
|
||||
query: GET_USER_INFO,
|
||||
variables: {
|
||||
userLogin: user.userLogin
|
||||
targetLogin: user.login,
|
||||
channelID: room.id
|
||||
}
|
||||
});
|
||||
|
||||
const [, mod_card_vue, user_info] = await Promise.all([vue.enable(), _mod_card_vue, _user_info]);
|
||||
// But we only wait on loading Vue, since we can show a loading indicator.
|
||||
await this.loadVue();
|
||||
|
||||
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`;
|
||||
// Display the card.
|
||||
this.unpinned_card = this.open_cards[user.login] = this.buildCard(
|
||||
room,
|
||||
user,
|
||||
user_info,
|
||||
pos_x,
|
||||
pos_y,
|
||||
);
|
||||
}
|
||||
|
||||
buildModCard(vue, user, room, currentUser) {
|
||||
this.log.info(user);
|
||||
const vueEl = new vue.Vue({
|
||||
|
||||
buildCard(room, user, data, pos_x, pos_y) {
|
||||
let child;
|
||||
const component = new this.vue.Vue({
|
||||
el: createElement('div'),
|
||||
render: h => {
|
||||
const vueModCard = h('mod-card', {
|
||||
activeTab: Object.keys(this.tabs)[0],
|
||||
render: h => h('viewer-card', {
|
||||
props: {
|
||||
tabs: this.tabs,
|
||||
user,
|
||||
room,
|
||||
currentUser,
|
||||
raw_user: user,
|
||||
data,
|
||||
|
||||
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;
|
||||
getFFZ: () => this,
|
||||
getZ: () => ++this.last_z
|
||||
},
|
||||
|
||||
on: {
|
||||
close: () => {
|
||||
this.open_mod_cards[user.login].remove();
|
||||
this.open_mod_cards[user.login] = null;
|
||||
const el = component.$el;
|
||||
el.remove();
|
||||
component.$destroy();
|
||||
|
||||
if ( this.unpinned_card === child )
|
||||
this.unpinned_card = null;
|
||||
|
||||
if ( this.open_cards[user.login] === child )
|
||||
this.open_cards[user.login] = null;
|
||||
|
||||
this.emit('tooltips:cleanup');
|
||||
},
|
||||
|
||||
pin: () => {
|
||||
if ( this.unpinned_card === child )
|
||||
this.unpinned_card = null;
|
||||
}
|
||||
});
|
||||
return vueModCard;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return vueEl.$el;
|
||||
child = component.$children[0];
|
||||
|
||||
const el = component.$el;
|
||||
el.style.top = `${pos_y}px`;
|
||||
el.style.left = `${pos_x}px`
|
||||
|
||||
const container = document.querySelector('.twilight-root>.tw-full-height,.twilight-minimal-root>.tw-full-height');
|
||||
container.appendChild(el);
|
||||
|
||||
requestAnimationFrame(() => child.constrain());
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
<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,3 @@
|
|||
.player:hover:not([data-controls="true"]):not([data-paused="true"]):not([data-ended="true"]) {
|
||||
cursor: none;
|
||||
}
|
|
@ -184,6 +184,15 @@ export default class Player extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('player.hide-mouse', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Channel > Player >> General',
|
||||
title: "Hide mouse when controls aren't visible.",
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: val => this.css_tweaks.toggle('player-hide-mouse', val)
|
||||
});
|
||||
}
|
||||
|
||||
updateHideExtensions(val) {
|
||||
|
@ -199,6 +208,7 @@ export default class Player extends Module {
|
|||
this.css_tweaks.toggle('player-ext-mouse', !this.settings.get('player.ext-interaction'));
|
||||
this.css_tweaks.toggle('theatre-no-whispers', this.settings.get('player.theatre.no-whispers'));
|
||||
this.css_tweaks.toggle('theatre-metadata', this.settings.get('player.theatre.metadata'));
|
||||
this.css_tweaks.toggle('player-hide-mouse', this.settings.get('player.hide-mouse'));
|
||||
this.css_tweaks.toggleHide('player-event-bar', this.settings.get('player.hide-event-bar'));
|
||||
this.css_tweaks.toggleHide('player-rerun-bar', this.settings.get('player.hide-rerun-bar'));
|
||||
this.updateHideExtensions();
|
||||
|
@ -257,6 +267,7 @@ export default class Player extends Module {
|
|||
process(inst) {
|
||||
this.addResetButton(inst);
|
||||
this.addEndedListener(inst);
|
||||
this.addStateTags(inst);
|
||||
this.addControlVisibility(inst);
|
||||
this.updateVolumeScroll(inst);
|
||||
}
|
||||
|
@ -298,6 +309,17 @@ export default class Player extends Module {
|
|||
|
||||
inst._ffz_autoplay_handler = null;
|
||||
}
|
||||
|
||||
if ( inst._ffz_on_state ) {
|
||||
if ( p ) {
|
||||
off(p, 'ended', inst._ffz_on_state);
|
||||
off(p, 'pause', inst._ffz_on_state);
|
||||
off(p, 'playing', inst._ffz_on_state);
|
||||
off(p, 'error', inst._ffz_on_state);
|
||||
}
|
||||
|
||||
inst._ffz_on_state = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -347,6 +369,40 @@ export default class Player extends Module {
|
|||
}
|
||||
|
||||
|
||||
addStateTags(inst) {
|
||||
const p = inst.player;
|
||||
if ( ! p )
|
||||
return;
|
||||
|
||||
if ( inst._ffz_on_state ) {
|
||||
off(p, 'ended', inst._ffz_on_state);
|
||||
off(p, 'pause', inst._ffz_on_state);
|
||||
off(p, 'playing', inst._ffz_on_state);
|
||||
off(p, 'error', inst._ffz_on_state);
|
||||
}
|
||||
|
||||
const f = inst._ffz_on_state = () => this.updateStateTags(inst);
|
||||
|
||||
on(p, 'ended', f);
|
||||
on(p, 'pause', f);
|
||||
on(p, 'playing', f);
|
||||
on(p, 'error', f);
|
||||
|
||||
f();
|
||||
}
|
||||
|
||||
|
||||
updateStateTags(inst) { // eslint-disable-line class-methods-use-this
|
||||
const p = inst.playerRef,
|
||||
player = inst.player;
|
||||
if ( ! p || ! player )
|
||||
return;
|
||||
|
||||
p.dataset.ended = player.ended;
|
||||
p.dataset.paused = player.paused;
|
||||
}
|
||||
|
||||
|
||||
disableAutoplay(inst) {
|
||||
const p = inst.player;
|
||||
if ( ! p )
|
||||
|
|
|
@ -111,10 +111,6 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: ffz-rotateplane 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ffz--menu-badge {
|
||||
width: 1.8rem; height: 1.8rem;
|
||||
}
|
||||
|
|
11
src/std-components/tooltip.vue
Normal file
11
src/std-components/tooltip.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template functional>
|
||||
<div class="tw-tooltip-wrapper">
|
||||
<slot />
|
||||
<div
|
||||
:class="`tw-tooltip--align-${props.align||'center'} tw-tooltip--${props.above ? 'up' : 'down'}`"
|
||||
class="tw-tooltip"
|
||||
>
|
||||
<slot name="tooltip" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -125,7 +125,8 @@ export default class Apollo extends Module {
|
|||
try {
|
||||
out.subscribe({
|
||||
next: result => {
|
||||
if ( result.errors ) {
|
||||
// Logging GQL errors is garbage. Don't do it.
|
||||
/*if ( result.errors ) {
|
||||
const name = operation.operationName;
|
||||
if ( name && (name.includes('FFZ') || has(this.modifiers, name) || has(this.post_modifiers, name)) ) {
|
||||
for(const err of result.errors) {
|
||||
|
@ -142,7 +143,7 @@ export default class Apollo extends Module {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
this.log.crumb({
|
||||
level: 'info',
|
||||
|
|
|
@ -127,6 +127,18 @@ export class Tooltip {
|
|||
}
|
||||
|
||||
|
||||
cleanup() {
|
||||
for(const el of this.elements) {
|
||||
const tip = el[this._accessor];
|
||||
if ( document.body.contains(el) )
|
||||
continue;
|
||||
|
||||
if ( tip && tip.visible )
|
||||
this.hide(tip);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_enter(target) {
|
||||
let tip = target[this._accessor];
|
||||
if ( ! tip )
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
.ffz-i-pin:before { content: '\e825'; } /* '' */
|
||||
.ffz-i-pin-outline:before { content: '\e826'; } /* '' */
|
||||
.ffz-i-gift:before { content: '\e827'; } /* '' */
|
||||
.ffz-i-views:before { content: '\e828'; } /* '' */
|
||||
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
|
|
|
@ -10,3 +10,8 @@
|
|||
75% { transform: rotateY(0deg) rotateX(0deg) }
|
||||
100% { transform: rotateY(90deg) rotateX(0deg) }
|
||||
}
|
||||
|
||||
|
||||
.ffz-i-zreknarf.loading {
|
||||
animation: ffz-rotateplane 1.2s infinite linear;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue