1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-01 16:48:32 +00:00

4.0.0-rc4.5

* Added: Whisper Support
* Fixed: UI missing hover state for a few elements added by FrankerFaceZ.
* Fixed: Handle missing badge definition when rendering FFZ badges.
* Fixed: Update static chat message type mappings.
* Fixed: Error in metadata when unable to get the proper player.
* Fixed: Incorrectly applying dark theme to products page.

A bit more work on getting enhanced viewer cards ready.
This commit is contained in:
SirStendec 2018-07-13 14:32:12 -04:00
parent 4a326823b9
commit 17fb41f083
26 changed files with 396 additions and 80 deletions

View file

@ -1,4 +1,14 @@
<div class="list-header">4.0.0-rc4.4<span>@9e5af5443a7601d78faf</span> <time datetime="2018-07-09">(2018-07-09)</time></div> <div class="list-header">4.0.0-rc4.5<span>@f47412afa7703e2d3b18</span> <time datetime="2018-07-13">(2018-07-13)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Whisper Support</li>
<li>Fixed: UI missing hover state for a few elements added by FrankerFaceZ.</li>
<li>Fixed: Handle missing badge definition when rendering FFZ badges.</li>
<li>Fixed: Update static chat message type mappings.</li>
<li>Fixed: Error in metadata when unable to get the proper player.</li>
<li>Fixed: Incorrectly applying dark theme to products page.</li>
</ul>
<div class="list-header">4.0.0-rc4.4<span>@46f98c4cd4559eaa9828</span> <time datetime="2018-07-05">(2018-07-05)</time></div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li>Changed: Make usernames clickable in resub notifications in chat, to match with native subscription messages.</li> <li>Changed: Make usernames clickable in resub notifications in chat, to match with native subscription messages.</li>
<li>Fixed: Make the code to automatically leave raids more robust.</li> <li>Fixed: Make the code to automatically leave raids more robust.</li>

View file

@ -135,9 +135,11 @@ export class TranslationManager extends Module {
} }
onEnable() { onEnable() {
this._ = new TranslationCore; /*({ this._ = new TranslationCore({
awarn: (...args) => this.log.info(...args) formatters: {
});*/ 'humanTime': n => this.toHumanTime(n)
}
});
this._.transformation = TRANSFORMATIONS[this.settings.get('i18n.debug.transform')]; this._.transformation = TRANSFORMATIONS[this.settings.get('i18n.debug.transform')];
this.locale = this.settings.get('i18n.locale'); this.locale = this.settings.get('i18n.locale');
@ -162,6 +164,9 @@ export class TranslationManager extends Module {
toHumanTime(duration, factor = 1) { toHumanTime(duration, factor = 1) {
// TODO: Make this better. Make all time handling better in fact. // TODO: Make this better. Make all time handling better in fact.
if ( duration instanceof Date )
duration = (Date.now() - duration.getTime()) / 1000;
duration = Math.floor(duration); duration = Math.floor(duration);
const years = Math.floor((duration * factor) / 31536000) / factor; const years = Math.floor((duration * factor) / 31536000) / factor;

View file

@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
FrankerFaceZ.Logger = Logger; FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = { const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-rc4.4', major: 4, minor: 0, revision: 0, extra: '-rc4.5',
build: __webpack_hash__, build: __webpack_hash__,
toString: () => toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}` `${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`

View file

@ -33,7 +33,7 @@
:key="i[0]" :key="i[0]"
:aria-checked="value.icon === i[0]" :aria-checked="value.icon === i[0]"
:class="{'tw-interactable--selected': value.icon === i[0]}" :class="{'tw-interactable--selected': value.icon === i[0]}"
class="ffz-icon tw-interactable" class="ffz-icon tw-interactable tw-interactable--inverted"
role="radio" role="radio"
tabindex="0" tabindex="0"
@keydown.space.stop.prevent="" @keydown.space.stop.prevent=""

View file

@ -57,6 +57,42 @@ export default class Actions extends Module {
} }
}); });
this.settings.add('chat.actions.viewer-card', {
// Filter out actions
process: (ctx, val) =>
val.filter(x => x.appearance &&
this.renderers[x.appearance.type] &&
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
(! x.action || this.actions[x.action])
),
default: [
{v: {action: 'friend'}},
{v: {action: 'whisper', appearance: {type: 'text', text: 'Whisper', button: true}}},
{v: {type: 'space'}},
{v: {action: 'card_menu'}},
{v: {type: 'new-line'}},
{v: {action: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, display: {mod: true}}},
{v: {action: 'timeout', appearance: {type: 'icon', icon: 'ffz-i-clock'}, display: {mod: true}}}
],
type: 'array_merge',
_ui: {
path: 'Chat > Viewer Cards >> tabs ~> Actions @{"description": "Here, you define what actions are available on viewer cards."}',
component: 'chat-actions',
data: () => {
const chat = this.resolve('site.chat');
return {
color: val => chat && chat.colors ? chat.colors.process(val) : val,
actions: deep_copy(this.actions),
renderers: deep_copy(this.renderers)
}
}
}
})
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
this.handleContext = this.handleContext.bind(this); this.handleContext = this.handleContext.bind(this);
} }
@ -216,7 +252,7 @@ export default class Actions extends Module {
}); });
return (<div return (<div
class="ffz--inline-actions tw-inline tw-mg-r-05" class="ffz--inline-actions ffz-action-data tw-inline tw-mg-r-05"
data-msg-id={msg.id} data-msg-id={msg.id}
data-user={user} data-user={user}
data-room={room} data-room={room}
@ -228,7 +264,7 @@ export default class Actions extends Module {
getData(element) { getData(element) {
const ds = element.dataset, const ds = element.dataset,
parent = element.parentElement, parent = element.closest('.ffz-action-data'),
pds = parent && parent.dataset, pds = parent && parent.dataset,
action = ds && ds.action, action = ds && ds.action,
definition = this.actions[action]; definition = this.actions[action];

View file

@ -387,7 +387,7 @@ export default class Badges extends Module {
if ( hidden_badges[badge.id] ) if ( hidden_badges[badge.id] )
continue; continue;
const full_badge = this.badges[badge.id], const full_badge = this.badges[badge.id] || {},
slot = has(badge, 'slot') ? badge.slot : full_badge.slot, slot = has(badge, 'slot') ? badge.slot : full_badge.slot,
old_badge = slotted[slot], old_badge = slotted[slot],
urls = badge.urls || (badge.image ? {1: badge.image} : null), urls = badge.urls || (badge.image ? {1: badge.image} : null),

View file

@ -566,6 +566,43 @@ export default class Chat extends Module {
} }
standardizeWhisper(msg) { // eslint-disable-line class-methods-use-this
if ( ! msg )
return msg;
if ( msg._ffz_message )
return msg._ffz_message;
const emotes = {},
is_action = msg.content.startsWith('/me '),
offset = is_action ? 4 : 0,
out = msg._ffz_message = {
user: msg.from,
message: msg.content.slice(offset),
is_action,
emotes,
timestamp: msg.sentAt && msg.sentAt.getTime(),
deleted: false
};
out.user.color = out.user.chatColor;
if ( Array.isArray(msg.emotes) && msg.emotes.length )
for(const emote of msg.emotes) {
const id = emote.emoteID,
em = emotes[id] = emotes[id] || [];
em.push({
startIndex: emote.from - offset,
endIndex: emote.to - offset
});
}
return out;
}
standardizeMessage(msg) { // eslint-disable-line class-methods-use-this standardizeMessage(msg) { // eslint-disable-line class-methods-use-this
if ( ! msg ) if ( ! msg )
return msg; return msg;
@ -574,6 +611,9 @@ export default class Chat extends Module {
if ( msg.sender && ! msg.user ) if ( msg.sender && ! msg.user )
msg.user = msg.sender; msg.user = msg.sender;
if ( msg.from && ! msg.user )
msg.user = msg.from;
let user = msg.user; let user = msg.user;
if ( ! user ) if ( ! user )
user = msg.user = {}; user = msg.user = {};

View file

@ -20,6 +20,7 @@ export default class Room {
this.style = new ManagedStyle(`room--${login}`); this.style = new ManagedStyle(`room--${login}`);
this.emote_sets = new SourcedSet; this.emote_sets = new SourcedSet;
this.badges = null;
this.users = {}; this.users = {};
this.user_ids = {}; this.user_ids = {};

View file

@ -928,7 +928,8 @@ export const TwitchEmotes = {
emotes = []; emotes = [];
for(const emote_id in data) for(const emote_id in data)
if ( has(data, emote_id) ) { // Disable fix for now so we can see what Twitch is sending for emote data.
if ( has(data, emote_id) ) { // && Array.isArray(data[emote_id]) ) {
for(const match of data[emote_id]) for(const match of data[emote_id])
emotes.push([emote_id, match.startIndex, match.endIndex + 1]); emotes.push([emote_id, match.startIndex, match.endIndex + 1]);
} }

View file

@ -254,7 +254,7 @@ export default {
}, },
canEdit() { canEdit() {
return this.action.v != null; return this.action.v != null && ! this.action.v.type;
}, },
canPreview() { canPreview() {
@ -268,6 +268,17 @@ export default {
else if ( ! this.display ) else if ( ! this.display )
return this.t('setting.unknown', 'Unknown Value'); return this.t('setting.unknown', 'Unknown Value');
const type = this.display.type;
if ( type === 'new-line' )
return this.t('setting.new-line', 'New Line');
else if ( type === 'space-small' )
return this.t('setting.space-small', 'Space (Small)');
else if ( type === 'space' )
return this.t('setting.space', 'Space');
const def = this.data.actions[this.display.action]; const def = this.data.actions[this.display.action];
if ( ! def ) if ( ! def )
return this.t('setting.actions.unknown', 'Unknown Action Type: %{action}', this.display); return this.t('setting.actions.unknown', 'Unknown Action Type: %{action}', this.display);
@ -289,6 +300,17 @@ export default {
if ( this.action.t === 'inherit' ) if ( this.action.t === 'inherit' )
return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles at this position.'); return this.t('setting.inheritance.desc', 'Inherit values from lower priority profiles at this position.');
const type = this.display && this.display.type;
if ( type === 'new-line' )
return this.t('setting.new-line.desc', 'Place all items following this on a new line.');
else if ( type === 'space-small' )
return this.t('setting.space-small.desc', 'Place a small space between the previous item and the next item.');
else if ( type === 'space' )
return this.t('setting.space.desc', 'Place as large a space as possible between the previous item and the next item.');
const def = this.display && this.data.actions[this.display.action]; const def = this.display && this.data.actions[this.display.action];
if ( ! def || ! def.description ) if ( ! def || ! def.description )
return; return;

View file

@ -27,7 +27,7 @@
</label> </label>
</div> </div>
<div class="tw-pd-x-1"> <div v-if="item.inline" class="tw-pd-x-1">
<input <input
id="is_deleted" id="is_deleted"
ref="is_deleted" ref="is_deleted"
@ -42,7 +42,7 @@
</label> </label>
</div> </div>
<div class="tw-pd-x-1"> <div v-if="item.inline" class="tw-pd-x-1">
<input <input
id="with_mod_icons" id="with_mod_icons"
ref="with_mod_icons" ref="with_mod_icons"
@ -76,18 +76,23 @@
<div <div
:data-user="JSON.stringify(sample_user)" :data-user="JSON.stringify(sample_user)"
:data-room="JSON.stringify(sample_room)" :data-room="JSON.stringify(sample_room)"
class="tw-flex tw-align-items-center tw-justify-content-center tw-pd-t-1" class="ffz-action-data tw-pd-t-1"
data-msg-id="1234-5678" data-msg-id="1234-5678"
> >
<div <div
v-if="! display.length" v-if="! display.length"
class="tw-c-text-alt-2 tw-pd-05 tw-font-size-4" class="tw-align-center tw-c-text-alt-2 tw-pd-05 tw-font-size-4"
> >
{{ t('setting.actions.no-visible', 'no visible actions') }} {{ t('setting.actions.no-visible', 'no visible actions') }}
</div> </div>
<div
v-for="(actions, idx) in display"
:key="idx"
class="tw-flex tw-align-items-center tw-justify-content-center"
>
<action-preview <action-preview
v-for="act in display" v-for="act in actions"
:key="act.id" :key="act.id"
:act="act.v" :act="act.v"
:color="color(act.v.appearance.color)" :color="color(act.v.appearance.color)"
@ -97,6 +102,7 @@
/> />
</div> </div>
</div> </div>
</div>
<div class="tw-flex tw-align-items-center tw-pd-b-05"> <div class="tw-flex tw-align-items-center tw-pd-b-05">
<div class="tw-flex-grow-1"> <div class="tw-flex-grow-1">
@ -135,7 +141,7 @@
v-else v-else
:key="idx" :key="idx"
:disabled="preset.disabled" :disabled="preset.disabled"
class="tw-interactable tw-full-width" class="tw-interactable tw-interactable--inverted tw-full-width"
@click="add(preset.value)" @click="add(preset.value)"
> >
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1"> <div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
@ -299,13 +305,25 @@ export default {
display() { display() {
const out = []; const out = [];
let current = [];
if ( this.val ) if ( this.val )
for(const val of this.val) { for(const val of this.val) {
if ( val.v && this.displayAction(val.v) ) if ( ! val.v )
out.push(val); continue;
const type = val.v.type;
if ( type === 'new-line' ) {
out.push(current);
current = [];
} else if ( this.displayAction(val.v) )
current.push(val);
} }
if ( current.length )
out.push(current);
return out; return out;
}, },
@ -386,8 +404,8 @@ export default {
this.show_all = this.$refs.show_all.checked; this.show_all = this.$refs.show_all.checked;
this.is_moderator = this.$refs.as_mod.checked; this.is_moderator = this.$refs.as_mod.checked;
this.is_staff = false; //this.$refs.as_staff.checked; this.is_staff = false; //this.$refs.as_staff.checked;
this.with_mod_icons = this.$refs.with_mod_icons.checked; this.with_mod_icons = this.item.inline && this.$refs.with_mod_icons.checked;
this.is_deleted = this.$refs.is_deleted.checked; this.is_deleted = this.item.inline && this.$refs.is_deleted.checked;
}, },
displayAction(action) { displayAction(action) {

View file

@ -220,6 +220,7 @@ export default class MainMenu extends Module {
return; return;
const tree = this._settings_tree, const tree = this._settings_tree,
expanded = this.settings.provider.get('settings-expanded', {}),
tokens = def.ui.path_tokens, tokens = def.ui.path_tokens,
len = tokens.length; len = tokens.length;
@ -244,6 +245,10 @@ export default class MainMenu extends Module {
}; };
Object.assign(token, raw_token); Object.assign(token, raw_token);
if ( has(expanded, key) )
token.expanded = expanded[key];
prefix = key; prefix = key;
} }

View file

@ -110,7 +110,7 @@ export default class Metadata extends Module {
const Player = this.resolve('site.player'), const Player = this.resolve('site.player'),
socket = this.resolve('socket'), socket = this.resolve('socket'),
player = Player.current, player = Player.current,
stats = player && player.getVideoInfo(); stats = player && maybe_call(player.getVideoInfo, player);
if ( ! stats ) if ( ! stats )
return {stats}; return {stats};

View file

@ -33,7 +33,7 @@
{{ login }} {{ login }}
</a> </a>
</h5> </h5>
<div v-if="loaded"> <div v-if="loaded" class="tw-pd-t-05">
<span <span
:data-title="t('viewer-card.views', 'Views')" :data-title="t('viewer-card.views', 'Views')"
class="ffz-tooltip tw-mg-r-05 ffz-i-views" class="ffz-tooltip tw-mg-r-05 ffz-i-views"
@ -47,10 +47,11 @@
{{ t(null, '%{followers|number}', {followers: user.followers.totalCount}) }} {{ t(null, '%{followers|number}', {followers: user.followers.totalCount}) }}
</span> </span>
<span <span
data-title="rawUserAge" v-if="userAge"
:data-title="t('viewer-card.age-tip', 'Member Since: %{age}', {age: userAge.toLocaleString()})"
class="ffz-tooltip ffz-i-clock" class="ffz-tooltip ffz-i-clock"
> >
{ userAge } {{ t('viewer-card.age', '%{age|humanTime}', {age: userAge}) }}
</span> </span>
</div> </div>
</div> </div>
@ -154,6 +155,13 @@ export default {
return this.raw_user.displayName || this.raw_user.login; return this.raw_user.displayName || this.raw_user.login;
}, },
userAge() {
if ( this.loaded )
return new Date(this.user.createdAt);
return null
},
current_tab() { current_tab() {
return this.tabs[this.active_tab]; return this.tabs[this.active_tab];
} }

View file

@ -54,10 +54,12 @@
</template> </template>
<script> <script>
import TabMixin from '../tab-mixin'; import TabMixin from '../tab-mixin';
export default { export default {
mixins: [TabMixin], mixins: [TabMixin],
props: ['tab', 'user', 'room', 'currentUser'] props: ['tab', 'user', 'room', 'currentUser']
} }
</script> </script>

View file

@ -4,8 +4,17 @@
<loading-tab v-else-if="loading" /> <loading-tab v-else-if="loading" />
<template v-else> <template v-else>
<ul> <ul>
<li v-for="entry in data" :key="entry[1]"> <li
<span>{{ entry[0] }}</span> v-for="(entry, idx) in data"
:key="entry[1]"
:class="idx === 0 ? '' : 'tw-border-t'"
class="tw-pd-x-1 tw-pd-y-05"
>
<span
:data-title="fullTime(entry[0])"
data-tooltip-type="text"
class="ffz-tooltip tw-pd-r-1"
>{{ formatTime(entry[0]) }}: </span>
{{ entry[1] }} {{ entry[1] }}
</li> </li>
</ul> </ul>
@ -44,12 +53,36 @@ export default {
socket.call('get_name_history', this.user.login).then(data => { socket.call('get_name_history', this.user.login).then(data => {
this.loading = false; this.loading = false;
if ( Array.isArray(data) )
data = data.reverse();
this.data = data; this.data = data;
}).catch(err => { }).catch(err => {
console.error(err); this.getFFZ().log.error('Error loading name history.', err);
this.errored = true; this.errored = true;
}) })
},
methods: {
fullTime(time) {
try {
const date = new Date(time);
return date.toLocaleString();
} catch(err) {
return 'Unknown'
}
},
formatTime(time) {
try {
const date = new Date(time);
return date.toLocaleDateString();
} catch(err) {
return 'Unknown'
}
}
} }
} }

View file

@ -55,10 +55,18 @@ export default class Twilight extends BaseSite {
this.router.on(':route', (route, match) => { this.router.on(':route', (route, match) => {
this.log.info('Navigation', route && route.name, match && match[0]); this.log.info('Navigation', route && route.name, match && match[0]);
this.fine.route(route && route.name); this.fine.route(route && route.name);
this.settings.updateContext({
route,
route_data: match
});
}); });
const current = this.router.current; const current = this.router.current;
this.fine.route(current && current.name); this.fine.route(current && current.name);
this.settings.updateContext({
route: current,
route_data: this.router.match
});
document.head.appendChild(createElement('link', { document.head.appendChild(createElement('link', {
href: MAIN_URL, href: MAIN_URL,
@ -171,5 +179,6 @@ Twilight.ROUTES = {
'user-events': '/:userName/events', 'user-events': '/:userName/events',
'user-followers': '/:userName/followers', 'user-followers': '/:userName/followers',
'user-following': '/:userName/following', 'user-following': '/:userName/following',
'product': '/products/:productName',
'user': '/:userName' 'user': '/:userName'
} }

View file

@ -6,7 +6,7 @@
import {ColorAdjuster} from 'utilities/color'; import {ColorAdjuster} from 'utilities/color';
import {setChildren} from 'utilities/dom'; import {setChildren} from 'utilities/dom';
import {has, split_chars} from 'utilities/object'; import {has, split_chars, shallow_object_equals} from 'utilities/object';
import {FFZEvent} from 'utilities/events'; import {FFZEvent} from 'utilities/events';
import Module from 'utilities/module'; import Module from 'utilities/module';
@ -73,31 +73,33 @@ const CHAT_TYPES = ((e = {}) => {
e[e.ModerationAction = 2] = 'ModerationAction'; e[e.ModerationAction = 2] = 'ModerationAction';
e[e.TargetedModerationAction = 3] = 'TargetedModerationAction'; e[e.TargetedModerationAction = 3] = 'TargetedModerationAction';
e[e.AutoMod = 4] = 'AutoMod'; e[e.AutoMod = 4] = 'AutoMod';
e[e.Connected = 5] = 'Connected'; e[e.SubscriberOnlyMode = 5] = 'SubscriberOnlyMode';
e[e.Disconnected = 6] = 'Disconnected'; e[e.FollowerOnlyMode = 6] = 'FollowerOnlyMode';
e[e.Reconnect = 7] = 'Reconnect'; e[e.SlowMode = 7] = 'SlowMode';
e[e.Hosting = 8] = 'Hosting'; e[e.EmoteOnlyMode = 8] = 'EmoteOnlyMode';
e[e.Unhost = 9] = 'Unhost'; e[e.R9KMode = 9] = 'R9KMode';
e[e.Hosted = 10] = 'Hosted'; e[e.Connected = 10] = 'Connected';
e[e.Subscription = 11] = 'Subscription'; e[e.Disconnected = 11] = 'Disconnected';
e[e.Resubscription = 12] = 'Resubscription'; e[e.Reconnect = 12] = 'Reconnect';
e[e.SubGift = 13] = 'SubGift'; e[e.Hosting = 13] = 'Hosting';
e[e.Clear = 14] = 'Clear'; e[e.Unhost = 14] = 'Unhost';
e[e.SubscriberOnlyMode = 15] = 'SubscriberOnlyMode'; e[e.Hosted = 15] = 'Hosted';
e[e.FollowerOnlyMode = 16] = 'FollowerOnlyMode'; e[e.Subscription = 16] = 'Subscription';
e[e.SlowMode = 17] = 'SlowMode'; e[e.Resubscription = 17] = 'Resubscription';
e[e.EmoteOnlyMode = 18] = 'EmoteOnlyMode'; e[e.SubGift = 18] = 'SubGift';
e[e.RoomMods = 19] = 'RoomMods'; e[e.Clear = 19] = 'Clear';
e[e.RoomState = 20] = 'RoomState'; e[e.RoomMods = 20] = 'RoomMods';
e[e.Raid = 21] = 'Raid'; e[e.RoomState = 21] = 'RoomState';
e[e.Unraid = 22] = 'Unraid'; e[e.Raid = 22] = 'Raid';
e[e.Ritual = 23] = 'Ritual'; e[e.Unraid = 23] = 'Unraid';
e[e.Notice = 24] = 'Notice'; e[e.Ritual = 24] = 'Ritual';
e[e.Info = 25] = 'Info'; e[e.Notice = 25] = 'Notice';
e[e.BadgesUpdated = 26] = 'BadgesUpdated'; e[e.Info = 26] = 'Info';
e[e.Purchase = 27] = 'Purchase'; e[e.BadgesUpdated = 27] = 'BadgesUpdated';
e[e.BitsCharity = 28] = 'BitsCharity'; e[e.Purchase = 28] = 'Purchase';
e[e.CrateGift = 29] = 'CrateGift' e[e.BitsCharity = 29] = 'BitsCharity';
e[e.CrateGift = 30] = 'CrateGift';
e[e.RewardGift = 31] = 'RewardGift';
return e; return e;
})(); })();
@ -354,12 +356,34 @@ export default class ChatHook extends Module {
async grabTypes() { async grabTypes() {
const ct = await this.web_munch.findModule('chat-types'); const ct = await this.web_munch.findModule('chat-types'),
changes = [];
this.automod_types = ct && ct.a || AUTOMOD_TYPES; if ( ! ct )
this.chat_types = ct && ct.b || CHAT_TYPES; return;
this.message_types = ct && ct.c || MESSAGE_TYPES;
this.mod_types = ct && ct.e || MOD_TYPES; if ( ct.a && ! shallow_object_equals(ct.a, AUTOMOD_TYPES) ) {
changes.push('AUTOMOD_TYPES');
this.automod_types = ct.a;
}
if ( ct.b && ! shallow_object_equals(ct.b, CHAT_TYPES) ) {
changes.push('CHAT_TYPES');
this.chat_types = ct.b;
}
if ( ct.c && ! shallow_object_equals(ct.c, MESSAGE_TYPES) ) {
changes.push('MESSAGE_TYPES');
this.message_types = ct.c;
}
if ( ct.e && ! shallow_object_equals(ct.e, MOD_TYPES) ) {
changes.push('MOD_TYPES');
this.mod_types = ct.e;
}
if ( changes.length )
this.log.info('Chat Types have changed from static mappings for categories:', changes.join(' '));
} }

View file

@ -42,6 +42,11 @@ export default class ChatLine extends Module {
n => n.renderMessageBody && n.props && n.props.roomID, n => n.renderMessageBody && n.props && n.props.roomID,
Twilight.CHAT_ROUTES Twilight.CHAT_ROUTES
); );
this.WhisperLine = this.fine.define(
'whisper-line',
n => n.props && n.props.message && n.props.reportOutgoingWhisperRendered
)
} }
async onEnable() { async onEnable() {
@ -157,7 +162,50 @@ export default class ChatLine extends Module {
});*/ });*/
this.WhisperLine.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
if ( ! this.props.message || ! this.props.message.content )
return old_render.call(this);
const msg = t.chat.standardizeWhisper(this.props.message),
is_action = msg.is_action,
user = msg.user,
color = t.parent.colors.process(user.color),
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, null, null),
contents = t.chat.renderTokens(tokens, e);
return e('div', {className: 'thread-message__message'},
e('div', {className: 'tw-pd-x-1 tw-pd-y-05'}, [
e('span', {
className: 'thread-message__message--user-name notranslate',
style: {
color
}
}, user.displayName),
e('span', null, is_action ? ' ' : ': '),
e('span', {
className: 'message',
style: {
color: is_action && color
}
}, contents)
])
);
}
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.WhisperLine.forceUpdate());
});
this.ChatLine.ready(cls => { this.ChatLine.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.shouldComponentUpdate = function(props, state) { cls.prototype.shouldComponentUpdate = function(props, state) {
const show = state.alwaysShowMessage || ! props.message.deleted, const show = state.alwaysShowMessage || ! props.message.deleted,
old_show = this._ffz_show; old_show = this._ffz_show;
@ -176,6 +224,8 @@ export default class ChatLine extends Module {
} }
cls.prototype.render = function() { cls.prototype.render = function() {
try {
const types = t.parent.message_types || {}, const types = t.parent.message_types || {},
msg = t.chat.standardizeMessage(this.props.message), msg = t.chat.standardizeMessage(this.props.message),
@ -233,7 +283,7 @@ export default class ChatLine extends Module {
bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null; bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
if ( ! this.ffz_user_click_handler ) if ( ! this.ffz_user_click_handler )
this.ffz_user_click_handler = this.usernameClickHandler; // event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event); this.ffz_user_click_handler = this.usernameClickHandler; // event => ! event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`, let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`,
out = (tokens.length || ! msg.ffz_type) ? [ out = (tokens.length || ! msg.ffz_type) ? [
@ -363,6 +413,16 @@ export default class ChatLine extends Module {
'data-user-id': user.userID, 'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(), 'data-user': user.userLogin && user.userLogin.toLowerCase(),
}, out); }, out);
} catch(err) {
t.log.capture(err, {
extra: {
props: this.props
}
});
return old_render.call(this);
}
} }
// Do this after a short delay to hopefully reduce the chance of React // Do this after a short delay to hopefully reduce the chance of React
@ -389,7 +449,14 @@ export default class ChatLine extends Module {
} }
} }
for(const inst of this.WhisperLine.instances) {
const msg = inst.props.message;
if ( msg && msg._ffz_message )
msg._ffz_message = null;
}
this.ChatLine.forceUpdate(); this.ChatLine.forceUpdate();
this.ChatRoomLine.forceUpdate(); this.ChatRoomLine.forceUpdate();
this.WhisperLine.forceUpdate();
} }
} }

View file

@ -260,8 +260,11 @@ export default class Scroller extends Module {
let step = this.ffz_smooth_scroll, let step = this.ffz_smooth_scroll,
old_time = Date.now(); old_time = Date.now();
const scroll_content = this.scroll.scrollContent, const scroll_content = this.scroll.scrollContent;
target_top = scroll_content.scrollHeight - scroll_content.clientHeight, if ( ! scroll_content )
return;
const target_top = scroll_content.scrollHeight - scroll_content.clientHeight,
difference = target_top - scroll_content.scrollTop; difference = target_top - scroll_content.scrollTop;
// If we are falling behind speed us up // If we are falling behind speed us up

View file

@ -61,7 +61,7 @@ export default class Following extends SiteModule {
] ]
}, },
changed: () => this.ChannelCard.forceUpdate() changed: () => this.parent.DirectoryCard.forceUpdate()
}); });
this.apollo.registerModifier('FollowedChannels_RENAME2', FOLLOWED_CHANNELS); this.apollo.registerModifier('FollowedChannels_RENAME2', FOLLOWED_CHANNELS);
@ -219,7 +219,7 @@ export default class Following extends SiteModule {
// Hosted Channel Content // Hosted Channel Content
simplebarContentChildren.push(<a simplebarContentChildren.push(<a
class="tw-interactable" class="tw-interactable tw-interactable--inverted"
href={`/${hostData.channel}`} href={`/${hostData.channel}`}
onClick={e => this.parent.hijackUserClick(e, hostData.channel, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind onClick={e => this.parent.hijackUserClick(e, hostData.channel, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
> >
@ -242,7 +242,7 @@ export default class Following extends SiteModule {
for (let i = 0; i < hostData.nodes.length; i++) { for (let i = 0; i < hostData.nodes.length; i++) {
const node = hostData.nodes[i]; const node = hostData.nodes[i];
simplebarContentChildren.push(<a simplebarContentChildren.push(<a
class="tw-interactable" class="tw-interactable tw-interactable--inverted"
href={`/${node.login}`} href={`/${node.login}`}
onClick={e => this.parent.hijackUserClick(e, node.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind onClick={e => this.parent.hijackUserClick(e, node.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
> >

View file

@ -26,7 +26,7 @@
:data-id="host._id" :data-id="host._id"
class="tw-border-t ffz--host-user" class="tw-border-t ffz--host-user"
> >
<div class="tw-interactable"> <div class="tw-interactable tw-interactable--inverted">
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1"> <div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1">
<figure class="ffz-i-ellipsis-vert handle" /> <figure class="ffz-i-ellipsis-vert handle" />
<div class="ffz-channel-avatar"> <div class="ffz-channel-avatar">

View file

@ -9,6 +9,8 @@ import {createElement} from 'utilities/dom';
import THEME_CSS_URL from 'site/styles/theme.scss'; import THEME_CSS_URL from 'site/styles/theme.scss';
const BAD_ROUTES = ['product'];
export default class ThemeEngine extends Module { export default class ThemeEngine extends Module {
constructor(...args) { constructor(...args) {
@ -17,14 +19,15 @@ export default class ThemeEngine extends Module {
this.inject('site'); this.inject('site');
this.inject('site.css_tweaks'); this.inject('site.css_tweaks');
this.inject('site.router');
this.should_enable = true; this.should_enable = true;
this.settings.add('theme.dark', { this.settings.add('theme.dark', {
requires: ['context.ui.theme'], requires: ['theme.is-dark'],
default: false, default: false,
process(ctx, val) { process(ctx, val) {
return ctx.get('context.ui.theme') === 1 ? val : false return ctx.get('theme.is-dark') ? val : false
}, },
ui: { ui: {
@ -37,10 +40,17 @@ export default class ThemeEngine extends Module {
changed: val => this.updateSetting(val) changed: val => this.updateSetting(val)
}); });
this.settings.add('theme.is-dark', { this.settings.add('theme.can-dark', {
requires: ['context.ui.theme'], requires: ['context.route.name'],
process(ctx) { process(ctx) {
return ctx.get('context.ui.theme') === 1; return ! BAD_ROUTES.includes(ctx.get('context.route.name'))
}
});
this.settings.add('theme.is-dark', {
requires: ['theme.can-dark', 'context.ui.theme'],
process(ctx) {
return ctx.get('theme.can-dark') && ctx.get('context.ui.theme') === 1;
}, },
changed: () => this.updateCSS() changed: () => this.updateCSS()
}); });

View file

@ -3,6 +3,8 @@
z-index: 9001; z-index: 9001;
> header { > header {
cursor: move;
background-size: cover; background-size: cover;
background-position: top; background-position: top;
} }

View file

@ -92,6 +92,18 @@ export function array_equals(a, b) {
} }
export function shallow_object_equals(a, b) {
if ( typeof a !== 'object' || typeof b !== 'object' || ! array_equals(Object.keys(a), Object.keys(b)) )
return false;
for(const key in a)
if ( a[key] !== b[key] )
return false;
return true;
}
export function set_equals(a,b) { export function set_equals(a,b) {
if ( !(a instanceof Set) || !(b instanceof Set) || a.size !== b.size ) if ( !(a instanceof Set) || !(b instanceof Set) || a.size !== b.size )
return false; return false;

View file

@ -71,6 +71,11 @@ export class Vue extends Module {
return t.i18n.t(key, phrase, options); return t.i18n.t(key, phrase, options);
}, },
tList_(key, phrase, options) {
this.locale && this.phrases[key];
return t.i18n.tList(key, phrase, options);
},
setLocale(locale) { setLocale(locale) {
t.i18n.locale = locale; t.i18n.locale = locale;
} }
@ -96,6 +101,9 @@ export class Vue extends Module {
methods: { methods: {
t(key, phrase, options) { t(key, phrase, options) {
return this.$i18n.t_(key, phrase, options); return this.$i18n.t_(key, phrase, options);
},
tList(key, phrase, options) {
return this.$i18n.tList_(key, phrase, options);
} }
} }
}); });