1
0
Fork 0
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:
SirStendec 2019-05-07 15:04:12 -04:00
parent c920b43e01
commit 5500b6eef3
14 changed files with 312 additions and 67 deletions

View file

@ -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: () =>

View file

@ -94,8 +94,6 @@ const FFZ_ICONS = [
'lock',
'lock-open',
'arrows-cw',
'pin',
'pin-outline',
'gift',
'eyedropper',
'github',

View file

@ -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 )

View file

@ -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)
}
}

View file

@ -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(', ');
},

View file

@ -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 || [];

View file

@ -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$/

View file

@ -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);

View file

@ -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);

View file

@ -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 )

View file

@ -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) => {

View file

@ -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') )

View file

@ -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;

View file

@ -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;