mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
4.0.0-rc20
* Added: Room Actions for Chat. Easily send canned messages or open relevant links. * Changed: Refactor how action data is passed to in-line chat actions. Should perform better now, and also allow using the message text in actions. * Changed: Blacklist a few errors from automatic error reporting. * Fixed: Include the Squad Bar when calculating the player height for Portrait Mode. * Fixed: Issue with rich content embeds breaking chat rendering when an error occurs loading their data. * Fixed: Duplicate icon keys in chat action editor.
This commit is contained in:
parent
c920b43e01
commit
5500b6eef3
14 changed files with 312 additions and 67 deletions
|
@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc19.4',
|
||||
major: 4, minor: 0, revision: 0, extra: '-rc20',
|
||||
commit: __git_commit__,
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
|
|
|
@ -94,8 +94,6 @@ const FFZ_ICONS = [
|
|||
'lock',
|
||||
'lock-open',
|
||||
'arrows-cw',
|
||||
'pin',
|
||||
'pin-outline',
|
||||
'gift',
|
||||
'eyedropper',
|
||||
'github',
|
||||
|
|
|
@ -90,6 +90,35 @@ export default class Actions extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.actions.room', {
|
||||
// Filter out actions
|
||||
process: (ctx, val) =>
|
||||
val.filter(x => x.type || (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: [],
|
||||
type: 'array_merge',
|
||||
ui: {
|
||||
path: 'Chat > Actions > Room @{"description": "Here, you can define custom actions that will appear above the chat input box."}',
|
||||
component: 'chat-actions',
|
||||
context: ['room'],
|
||||
inline: true,
|
||||
|
||||
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.settings.add('chat.actions.rules-as-reasons', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -346,6 +375,56 @@ export default class Actions extends Module {
|
|||
}
|
||||
|
||||
|
||||
renderRoom(mod_icons, current_user, current_room, createElement) {
|
||||
const actions = [],
|
||||
chat = this.resolve('site.chat');
|
||||
|
||||
for(const data of this.parent.context.get('chat.actions.room')) {
|
||||
if ( ! data || ! data.action || ! data.appearance )
|
||||
continue;
|
||||
|
||||
const ap = data.appearance || {},
|
||||
disp = data.display || {},
|
||||
|
||||
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.mod : false)) ||
|
||||
(disp.staff != null && disp.staff !== (current_user ? !!current_user.staff : false)) )
|
||||
continue;
|
||||
|
||||
const has_color = def.colored && ap.color,
|
||||
color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color),
|
||||
contents = def.render.call(this, ap, createElement, color);
|
||||
|
||||
actions.push(<button
|
||||
class={`ffz-tooltip ffz-mod-icon mod-icon tw-c-text-alt-2${has_color ? ' colored' : ''}`}
|
||||
data-tooltip-type="action"
|
||||
data-action={data.action}
|
||||
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||
data-tip={ap.tooltip}
|
||||
onClick={this.handleClick}
|
||||
onContextMenu={this.handleContext}
|
||||
>
|
||||
{contents}
|
||||
</button>);
|
||||
}
|
||||
|
||||
if ( ! actions.length )
|
||||
return null;
|
||||
|
||||
const room = current_room && JSON.stringify(current_room);
|
||||
|
||||
return (<div
|
||||
class="ffz--room-actions ffz-action-data tw-pd-y-05 tw-border-t"
|
||||
data-room={room}
|
||||
>
|
||||
{actions}
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
renderInline(msg, mod_icons, current_user, current_room, createElement) {
|
||||
const actions = [];
|
||||
|
||||
|
@ -393,19 +472,17 @@ export default class Actions extends Module {
|
|||
if ( ! actions.length )
|
||||
return null;
|
||||
|
||||
const room = current_room && JSON.stringify(current_room),
|
||||
/*const room = current_room && JSON.stringify(current_room),
|
||||
user = msg.user && JSON.stringify({
|
||||
login: msg.user.login,
|
||||
displayName: msg.user.displayName,
|
||||
id: msg.user.id,
|
||||
type: msg.user.type
|
||||
});
|
||||
});*/
|
||||
|
||||
return (<div
|
||||
class="ffz--inline-actions ffz-action-data tw-inline-block tw-mg-r-05"
|
||||
data-msg-id={msg.id}
|
||||
data-user={user}
|
||||
data-room={room}
|
||||
data-source="line"
|
||||
>
|
||||
{actions}
|
||||
</div>);
|
||||
|
@ -422,18 +499,55 @@ export default class Actions extends Module {
|
|||
if ( ! definition )
|
||||
return null;
|
||||
|
||||
const user = pds && pds.user ? JSON.parse(pds.user) : null,
|
||||
room = pds && pds.room ? JSON.parse(pds.room) : null,
|
||||
message_id = pds && pds.msgId,
|
||||
let user, room, message, loaded = false;
|
||||
|
||||
data = {
|
||||
if ( pds ) {
|
||||
if ( pds.source === 'line' ) {
|
||||
const fine = this.resolve('site.fine'),
|
||||
react = fine && fine.getParent(parent.parentElement),
|
||||
line = react && react.stateNode;
|
||||
|
||||
if ( line && line.props && line.props.message ) {
|
||||
loaded = true;
|
||||
|
||||
const msg = line.props.message;
|
||||
|
||||
user = msg.user ? {
|
||||
color: msg.user.color,
|
||||
id: msg.user.id,
|
||||
login: msg.user.login,
|
||||
displayName: msg.user.displayName,
|
||||
type: msg.user.type
|
||||
} : null;
|
||||
|
||||
room = {
|
||||
login: line.props.channelLogin,
|
||||
id: line.props.channelID
|
||||
}
|
||||
|
||||
message = {
|
||||
id: msg.id,
|
||||
text: msg.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! loaded ) {
|
||||
user = pds.user ? JSON.parse(pds.user) : null;
|
||||
room = pds.room ? JSON.parse(pds.room) : null;
|
||||
message = pds.message ? JSON.parse(pds.message) : pds.msgId ? {id: pds.msgId} : null;
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
action,
|
||||
definition,
|
||||
tip: ds.tip,
|
||||
options: ds.options ? JSON.parse(ds.options) : null,
|
||||
user,
|
||||
room,
|
||||
message_id
|
||||
message,
|
||||
message_id: message ? message.id : null
|
||||
};
|
||||
|
||||
if ( definition.defaults )
|
||||
|
|
|
@ -41,7 +41,7 @@ export const Links = {
|
|||
return {
|
||||
url: token.url,
|
||||
title: this.i18n.t('card.error', 'An error occurred.'),
|
||||
desc_1: err
|
||||
desc_1: String(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-align-items-center">
|
||||
<div v-if="has_message" class="tw-flex tw-align-items-center">
|
||||
<label for="vis_deleted">
|
||||
{{ t('setting.actions.edit-visible.deleted', 'Message Deleted') }}
|
||||
</label>
|
||||
|
@ -215,7 +215,7 @@
|
|||
import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['action', 'data', 'inline'],
|
||||
props: ['action', 'data', 'inline', 'context'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
@ -233,14 +233,30 @@ export default {
|
|||
return this.action.v;
|
||||
},
|
||||
|
||||
has_message() {
|
||||
return this.context && this.context.includes('message')
|
||||
},
|
||||
|
||||
vars() {
|
||||
const out = ['user.login', 'user.displayName', 'user.id', 'user.type'];
|
||||
const out = [],
|
||||
ctx = this.context || [];
|
||||
|
||||
out.push('room.login')
|
||||
if ( ctx.includes('user') ) {
|
||||
out.push('user.login');
|
||||
out.push('user.displayName');
|
||||
out.push('user.id');
|
||||
out.push('user.type');
|
||||
}
|
||||
|
||||
if ( ctx.includes('room') ) {
|
||||
out.push('room.login');
|
||||
out.push('room.id');
|
||||
}
|
||||
|
||||
if ( this.inline )
|
||||
out.push('message_id');
|
||||
if ( ctx.includes('message') ) {
|
||||
out.push('message.id');
|
||||
out.push('message.text');
|
||||
}
|
||||
|
||||
return out.map(x => `{{${x}}}`).join(', ');
|
||||
},
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="item.inline" class="tw-pd-x-1 tw-checkbox">
|
||||
<div v-if="item.inline && has_msg" class="tw-pd-x-1 tw-checkbox">
|
||||
<input
|
||||
id="is_deleted"
|
||||
ref="is_deleted"
|
||||
|
@ -76,6 +76,7 @@
|
|||
<div
|
||||
:data-user="JSON.stringify(sample_user)"
|
||||
:data-room="JSON.stringify(sample_room)"
|
||||
:data-message="JSON.stringify(sample_message)"
|
||||
class="ffz-action-data tw-pd-t-1"
|
||||
data-msg-id="1234-5678"
|
||||
>
|
||||
|
@ -169,7 +170,7 @@
|
|||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="! val.length"
|
||||
v-if="! val.length && has_default"
|
||||
class="tw-mg-l-1 tw-button tw-button--text tw-tooltip-wrapper"
|
||||
@click="populate"
|
||||
>
|
||||
|
@ -191,6 +192,7 @@
|
|||
:action="act"
|
||||
:data="data"
|
||||
:inline="item.inline"
|
||||
:context="item.context"
|
||||
@remove="remove(act)"
|
||||
@save="save(act, $event)"
|
||||
/>
|
||||
|
@ -222,18 +224,6 @@ export default {
|
|||
show_all: false,
|
||||
|
||||
add_open: false,
|
||||
|
||||
sample_user: {
|
||||
displayName: 'SirStendec',
|
||||
login: 'sirstendec',
|
||||
id: 49399878
|
||||
},
|
||||
|
||||
sample_room: {
|
||||
displayName: 'FrankerFaceZ',
|
||||
login: 'frankerfacez',
|
||||
id: 46622312
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -244,6 +234,46 @@ export default {
|
|||
return true;
|
||||
},
|
||||
|
||||
sample_user() {
|
||||
return this.has_user ? {
|
||||
displayName: 'SirStendec',
|
||||
login: 'sirstendec',
|
||||
id: 49399878,
|
||||
color: '#008000'
|
||||
} : null
|
||||
},
|
||||
|
||||
sample_room() {
|
||||
return this.has_room ? {
|
||||
displayName: 'FrankerFaceZ',
|
||||
login: 'frankerfacez',
|
||||
id: 46622312
|
||||
} : null
|
||||
},
|
||||
|
||||
sample_message() {
|
||||
return this.has_msg ? {
|
||||
id: '46a473ee-a3c4-4556-a5ca-c0f1eac93ec0',
|
||||
text: 'sirstendec: Please do not do that.'
|
||||
} : null
|
||||
},
|
||||
|
||||
has_default() {
|
||||
return this.default_value && this.default_value.length
|
||||
},
|
||||
|
||||
has_user() {
|
||||
return this.item.context && this.item.context.includes('user')
|
||||
},
|
||||
|
||||
has_room() {
|
||||
return this.item.context && this.item.context.includes('room')
|
||||
},
|
||||
|
||||
has_msg() {
|
||||
return this.item.context && this.item.context.includes('message')
|
||||
},
|
||||
|
||||
presets() {
|
||||
const out = [],
|
||||
contexts = this.item.context || [];
|
||||
|
|
|
@ -138,7 +138,9 @@ export default class RavenLogger extends Module {
|
|||
'InvalidAccessError',
|
||||
'out of memory',
|
||||
'Access is denied.',
|
||||
'Zugriff verweigert'
|
||||
'Zugriff verweigert',
|
||||
'freed script',
|
||||
'ffzenhancing'
|
||||
],
|
||||
sanitizeKeys: [
|
||||
/Token$/
|
||||
|
|
|
@ -212,6 +212,36 @@ export class LocalStorageProvider extends SettingsProvider {
|
|||
}
|
||||
|
||||
|
||||
export class IndexedDBProvider extends SettingsProvider {
|
||||
constructor(manager) {
|
||||
super(manager);
|
||||
|
||||
this._cached = new Map;
|
||||
this.ready = false;
|
||||
this._ready_wait = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.disable();
|
||||
this._cached.clear();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
awaitReady() {
|
||||
if ( this.ready )
|
||||
return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiters = this._ready_wait = this._ready_wait || [];
|
||||
waiters.push([resolve, reject]);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CloudStorageProvider extends SettingsProvider {
|
||||
constructor(manager) {
|
||||
super(manager);
|
||||
|
|
|
@ -112,7 +112,8 @@ export default class Twilight extends BaseSite {
|
|||
this.settings.updateContext({
|
||||
location: history && history.location,
|
||||
ui: state && state.ui,
|
||||
session: state && state.session
|
||||
session: state && state.session,
|
||||
chat: state && state.chat
|
||||
});
|
||||
} catch(err) {
|
||||
this.log.error('Error updating context.', err);
|
||||
|
|
|
@ -17,7 +17,7 @@ import Scroller from './scroller';
|
|||
import ChatLine from './line';
|
||||
import SettingsMenu from './settings_menu';
|
||||
import EmoteMenu from './emote_menu';
|
||||
import TabCompletion from './tab_completion';
|
||||
import Input from './input';
|
||||
|
||||
|
||||
const REGEX_EMOTES = {
|
||||
|
@ -150,7 +150,7 @@ export default class ChatHook extends Module {
|
|||
this.inject(ChatLine);
|
||||
this.inject(SettingsMenu);
|
||||
this.inject(EmoteMenu);
|
||||
this.inject(TabCompletion);
|
||||
this.inject(Input);
|
||||
|
||||
this.ChatService = this.fine.define(
|
||||
'chat-service',
|
||||
|
@ -560,25 +560,12 @@ export default class ChatHook extends Module {
|
|||
|
||||
this.ChatContainer.on('mount', this.containerMounted, this);
|
||||
this.ChatContainer.on('unmount', this.removeRoom, this);
|
||||
this.ChatContainer.on('receive-props', this.containerUpdated, this);
|
||||
this.ChatContainer.on('update', this.containerUpdated, this);
|
||||
|
||||
this.ChatContainer.ready((cls, instances) => {
|
||||
const t = this,
|
||||
old_render = cls.prototype.render,
|
||||
old_catch = cls.prototype.componentDidCatch;
|
||||
|
||||
// This is so stupid. I hate React. Why won't the events just fire
|
||||
// like they should.
|
||||
cls.prototype.render = function() {
|
||||
try {
|
||||
t.containerUpdated(this, this.props);
|
||||
} catch(err) {
|
||||
t.log.error(err);
|
||||
}
|
||||
|
||||
return old_render.call(this);
|
||||
}
|
||||
|
||||
// Try catching errors. With any luck, maybe we can
|
||||
// recover from the error when we re-build?
|
||||
cls.prototype.componentDidCatch = function(err, info) {
|
||||
|
@ -685,7 +672,7 @@ export default class ChatHook extends Module {
|
|||
if ( event.defaultPrevented || m.ffz_removed )
|
||||
return;
|
||||
|
||||
} else if ( msg.type === types.ModerationAction ) {
|
||||
} else if ( msg.type === types.ModerationAction && inst.markUserEventDeleted && inst.unsetModeratedUser ) {
|
||||
//t.log.info('Moderation Action', msg);
|
||||
if ( ! inst.props.isCurrentUserModerator )
|
||||
return;
|
||||
|
@ -724,7 +711,7 @@ export default class ChatHook extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
} else if ( msg.type === types.Moderation ) {
|
||||
} else if ( msg.type === types.Moderation && inst.markUserEventDeleted && inst.unsetModeratedUser ) {
|
||||
//t.log.info('Moderation', msg);
|
||||
if ( inst.props.isCurrentUserModerator )
|
||||
return;
|
||||
|
@ -1540,6 +1527,8 @@ export default class ChatHook extends Module {
|
|||
|
||||
|
||||
containerUpdated(cont, props) {
|
||||
// If we don't have a room, or if the room ID doesn't match our ID
|
||||
// then we need to just create a new Room because the chat room changed.
|
||||
if ( ! cont._ffz_room || props.channelID != cont._ffz_room.id ) {
|
||||
this.removeRoom(cont);
|
||||
if ( cont._ffz_mounted )
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
import Module from 'utilities/module';
|
||||
import Twilight from 'site';
|
||||
|
||||
export default class TabCompletion extends Module {
|
||||
export default class Input extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('chat');
|
||||
this.inject('chat.actions');
|
||||
this.inject('chat.emotes');
|
||||
this.inject('chat.emoji');
|
||||
this.inject('i18n');
|
||||
|
@ -59,15 +60,58 @@ export default class TabCompletion extends Module {
|
|||
}
|
||||
|
||||
async onEnable() {
|
||||
this.chat.context.on('changed:chat.actions.room', () => this.ChatInput.forceUpdate());
|
||||
|
||||
const React = await this.web_munch.findModule('react'),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
if ( ! createElement )
|
||||
return this.log.warn('Unable to get React.');
|
||||
|
||||
const t = this;
|
||||
|
||||
this.ChatInput.ready((cls, instances) => {
|
||||
for(const inst of instances)
|
||||
const old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
const out = old_render.call(this);
|
||||
try {
|
||||
if ( ! out || ! out.props || ! Array.isArray(out.props.children) )
|
||||
return out;
|
||||
|
||||
const props = this.props;
|
||||
if ( ! props || ! props.channelID )
|
||||
return out;
|
||||
|
||||
const u = props.sessionUser ? {
|
||||
id: props.sessionUser.id,
|
||||
login: props.sessionUser.login,
|
||||
displayName: props.sessionUser.displayName,
|
||||
mod: props.isCurrentUserModerator,
|
||||
staff: props.isStaff
|
||||
} : null,
|
||||
r = {
|
||||
id: props.channelID,
|
||||
login: props.channelLogin,
|
||||
displayName: props.channelDisplayName
|
||||
}
|
||||
|
||||
const actions = t.actions.renderRoom(t.chat.context.get('context.chat.showModIcons'), u, r, createElement);
|
||||
if ( actions )
|
||||
out.props.children.unshift(actions);
|
||||
|
||||
} catch(err) {
|
||||
t.log.error(err);
|
||||
t.log.capture(err);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
for(const inst of instances) {
|
||||
inst.forceUpdate();
|
||||
this.updateEmoteCompletion(inst);
|
||||
}
|
||||
});
|
||||
|
||||
this.EmoteSuggestions.ready((cls, instances) => {
|
|
@ -92,7 +92,7 @@ export default class Layout extends Module {
|
|||
});
|
||||
|
||||
this.settings.add('layout.portrait-extra-height', {
|
||||
requires: ['context.new_channel', 'context.hosting', 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
requires: ['context.new_channel', 'context.squad_bar', 'context.hosting', 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
|
||||
process(ctx) {
|
||||
let height = 0;
|
||||
if ( ctx.get('context.ui.theatreModeEnabled') ) {
|
||||
|
@ -107,6 +107,9 @@ export default class Layout extends Module {
|
|||
if ( ctx.get('whispers.show') )
|
||||
height += 4;
|
||||
|
||||
if ( ctx.get('context.squad_bar') )
|
||||
height += 6;
|
||||
|
||||
height += ctx.get('context.new_channel') ? 1 : 5;
|
||||
|
||||
if ( ctx.get('context.hosting') )
|
||||
|
|
|
@ -402,7 +402,12 @@ export default class Player extends Module {
|
|||
}
|
||||
|
||||
this.SquadStreamBar.forceUpdate();
|
||||
})
|
||||
this.updateSquadContext();
|
||||
});
|
||||
|
||||
this.SquadStreamBar.on('mount', this.updateSquadContext, this);
|
||||
this.SquadStreamBar.on('update', this.updateSquadContext, this);
|
||||
this.SquadStreamBar.on('unmount', this.updateSquadContext, this);
|
||||
|
||||
this.Player.on('mount', this.onMount, this);
|
||||
this.Player.on('unmount', this.onUnmount, this);
|
||||
|
@ -437,6 +442,19 @@ export default class Player extends Module {
|
|||
}
|
||||
|
||||
|
||||
updateSquadContext() {
|
||||
this.settings.updateContext({
|
||||
squad_bar: this.hasSquadBar()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
hasSquadBar() {
|
||||
const inst = this.SquadStreamBar.first;
|
||||
return inst ? inst.shouldRenderSquadBanner(inst.props) : false
|
||||
}
|
||||
|
||||
|
||||
overrideInitialize(inst) {
|
||||
const t = this,
|
||||
old_init = inst.initializePlayer;
|
||||
|
|
|
@ -140,7 +140,7 @@ export default class SocketClient extends Module {
|
|||
_reconnect() {
|
||||
if ( ! this._reconnect_timer ) {
|
||||
if ( this._delay < 60000 )
|
||||
this._delay += (Math.floor(Math.random() * 10) + 5) * 1000;
|
||||
this._delay += (Math.floor(Math.random() * 15) + 5) * 1000;
|
||||
else
|
||||
this._delay = (Math.floor(Math.random() * 60) + 30) * 1000;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue