diff --git a/package.json b/package.json index 54411e83..36171324 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.70.0", + "version": "4.71.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/actions/actions.jsx b/src/modules/chat/actions/actions.jsx index 50f4f3af..54d07018 100644 --- a/src/modules/chat/actions/actions.jsx +++ b/src/modules/chat/actions/actions.jsx @@ -1218,8 +1218,6 @@ export default class Actions extends Module { if ( target._ffz_tooltip ) target._ffz_tooltip.hide(); - - return data.definition.click.call(this, event, data); } diff --git a/src/modules/chat/actions/components/edit-profile.vue b/src/modules/chat/actions/components/edit-profile.vue new file mode 100644 index 00000000..e2e7bf4c --- /dev/null +++ b/src/modules/chat/actions/components/edit-profile.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index 8a399be4..c50ad17e 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -650,6 +650,104 @@ export const whisper = { } +// ============================================================================ +// Toggle Profile +// ============================================================================ + +export const toggle_profile = { + presets: [{ + appearance: { + type: 'dynamic' + } + }], + + editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-profile.vue'), + + required_context: [], + supports_dynamic: true, + can_self: true, + + title: 'Toggle Settings Profile', + description(data) { + const ffz = window.FrankerFaceZ.get(), + settings = ffz?.resolve('settings'), + profile = settings?.profile(data.options?.uuid); + + if ( ! profile ) + return null; + + return profile.i18n_key ? this.t(profile.i18n_key, profile.name) : profile.name; + }, + description_i18n: null, + + dynamicAppearance(ap, data) { + const profile = this.settings.profile(data.options?.uuid); + if ( ! profile ) + return { + type: 'icon', + icon: 'ffz-i-attention', + color: ap.color + }; + + return { + type: 'icon', + icon: profile.toggled ? 'ffz-i-ok' : 'ffz-i-cancel', + color: ap.color + } + }, + + tooltip(data) { + const profile = this.settings.profile(data.options?.uuid); + let name; + if ( ! profile ) + name = 'invalid'; + else + name = profile.i18n_key ? this.i18n.t(profile.i18n_key, profile.name) : profile.name; + + return this.i18n.t('chat.actions.toggle_profile.tooltip', 'Toggle Profile: {name}', { + name + }); + }, + + click(event, data) { + const profile = this.settings.profile(data.options?.uuid); + if ( ! profile ) + return; + + // TODO: Make dynamic appearance things update automatically so + // we don't need this hack. + + // Find any setting using this. + const found = new Set; + for(const ctx of ['inline', 'hover', 'user-context', 'room']) { + const key = `chat.actions.${ctx}`, + stuff = this.parent.context.get(key); + + if ( Array.isArray(stuff) ) + for(const item of stuff) { + if ( item.action === 'toggle_profile' ) { + if ( item.options?.uuid === profile.uuid ) { + found.add(ctx); + this.parent.context.once(`changed:${key}`, () => found.delete(ctx)); + break; + } + } + } + } + + // Toggle the profile. + profile.toggled = ! profile.toggled; + + // Now wait, and update any setting that didn't update. + requestAnimationFrame(() => { + for(const ctx of found) { + this.parent.context.update(`chat.actions.${ctx}`); + } + }); + } +} + + // ============================================================================ // Gift Subscription // ============================================================================ @@ -675,4 +773,4 @@ export const gift_sub = { Woop woop. ); } -}*/ \ No newline at end of file +}*/ diff --git a/src/settings/index.ts b/src/settings/index.ts index 754e33a2..2a9efcf9 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -6,7 +6,7 @@ import { DEBUG } from 'utilities/constants'; import Module, { GenericModule, buildAddonProxy } from 'utilities/module'; -import {deep_equals, has, debounce, deep_copy} from 'utilities/object'; +import {deep_equals, has, debounce, deep_copy, generateUUID} from 'utilities/object'; import {PathNode, parse as parse_path} from 'utilities/path-parser'; import SettingsProfile from './profile'; @@ -132,6 +132,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> /** @internal */ __profiles: SettingsProfile[]; private __profile_ids: Record; + private __profile_uuids: Record; /** * Whether or not profiles have been disabled for this session @@ -175,6 +176,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> this.__contexts = []; this.__profiles = []; this.__profile_ids = {}; + this.__profile_uuids = {}; this.ui_structures = new Map; this.definitions = new Map; @@ -858,10 +860,10 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> /** * Get an existing {@link SettingsProfile} instance. - * @param {number} id - The id of the profile. + * @param id - The id or uuid of the profile. */ - profile(id: number): SettingsProfile | null { - return this.__profile_ids[id] ?? null; + profile(id: number | string): SettingsProfile | null { + return this.__profile_uuids[id] ?? this.__profile_ids[id as number] ?? null; } @@ -871,10 +873,12 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> */ loadProfiles(suppress_events: boolean = false) { const old_profile_ids = this.__profile_ids, + old_profile_uuids = this.__profile_uuids, old_profiles = this.__profiles, profile_ids: Record = this.__profile_ids = {}, profiles: SettingsProfile[] = this.__profiles = [], + profile_uuids: Record = this.__profile_uuids = {}, // Create a set of actual IDs with a map from the profiles // list rather than just getting the keys from the ID map @@ -899,6 +903,25 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> ]; } + // Update: Add UUIDs to all profiles. + let need_save = false; + + for(const profile of raw_profiles) { + if ( ! profile.uuid ) { + need_save = true; + + if ( profile.i18n_key === SettingsProfile.Default.i18n_key ) + profile.uuid = SettingsProfile.Default.uuid as string; + else if ( profile.i18n_key === SettingsProfile.Moderation.i18n_key ) + profile.uuid = SettingsProfile.Moderation.uuid as string; + else + profile.uuid = generateUUID(); + } + } + + if ( need_save ) + this.provider?.set('profiles', raw_profiles); + let reordered = false, changed = false; @@ -918,23 +941,13 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> if ( old_slot_id !== slot_id ) reordered = true; - // Monkey patch to the new profile format... - // Update: Probably safe to remove this, at this point. - /* - if ( profile_data.context && ! Array.isArray(profile_data.context) ) { - if ( profile_data.context.moderator ) - profile_data.context = SettingsProfile.Moderation.context; - else - profile_data.context = null; - } - */ - if ( old_profile && deep_equals(old_profile.data, profile_data, true) ) { // Did the order change? if ( old_slot_id !== slot_id ) changed = true; profiles.push(profile_ids[id] = old_profile); + profile_uuids[old_profile.uuid] = old_profile; continue; } @@ -947,6 +960,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> new_ids.add(id); profiles.push(new_profile); + profile_uuids[new_profile.uuid] = new_profile; changed = true; } @@ -994,11 +1008,13 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> } options.id = id; + options.uuid = generateUUID(); if ( ! options.name ) options.name = `Unnamed Profile ${this.__profiles.length + 1}`; const profile = this.__profile_ids[id] = new SettingsProfile(this, options); + this.__profile_uuids[options.uuid] = profile; this.__profiles.unshift(profile); profile.on('toggled', this._onProfileToggled, this); @@ -1040,6 +1056,7 @@ export default class SettingsManager extends Module<'settings', SettingsEvents> profile.off('toggled', this._onProfileToggled, this); profile.clear(); this.__profile_ids[id] = null; + this.__profile_uuids[profile.uuid] = null; const idx = this.__profiles.indexOf(profile); if ( idx !== -1 ) diff --git a/src/settings/profile.ts b/src/settings/profile.ts index 31919d52..d5d30c30 100644 --- a/src/settings/profile.ts +++ b/src/settings/profile.ts @@ -34,6 +34,7 @@ export default class SettingsProfile extends EventEmitter { static Default: Partial = { id: 0, + uuid: 'ffz_profile_default', name: 'Default Profile', i18n_key: 'setting.profiles.default', @@ -43,6 +44,7 @@ export default class SettingsProfile extends EventEmitter { static Moderation: Partial = { id: 1, + uuid: 'ffz_profile_moderation', name: 'Moderation', i18n_key: 'setting.profiles.moderation', @@ -78,6 +80,11 @@ export default class SettingsProfile extends EventEmitter { */ id: number = -1; + /** + * The unique ID for this profile. UUIDs should always be unique. + */ + uuid: string = null as any; + // Metadata /** @@ -172,6 +179,7 @@ export default class SettingsProfile extends EventEmitter { return { id: this.id, //parent: this.parent, + uuid: this.uuid, name: this.name, i18n_key: this.i18n_key, @@ -260,6 +268,7 @@ export default class SettingsProfile extends EventEmitter { // We don't want to override general settings. delete data.profile.ephemeral; delete data.profile.id; + delete data.profile.uuid; delete data.profile.name; delete data.profile.i18n_key; delete data.profile.hotkey; diff --git a/src/settings/types.ts b/src/settings/types.ts index 4b427a71..68492695 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -280,6 +280,7 @@ export type ExportedBlobMetadata = { export type SettingsProfileMetadata = { id: number; + uuid: string; name: string; i18n_key?: string | null; diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 6864991f..d995d40b 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -833,6 +833,7 @@ export default class Input extends Module { out.push({ current: input, replacement: inst.determineReplacement(cmd), + description: cmd.description, element: inst.renderCommandSuggestion(cmd, i), group: cmd.ffz_group ? (Array.isArray(cmd.ffz_group) ? t.i18n.t(...cmd.ffz_group) : cmd.ffz_group) diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 63df9dd2..1d391db3 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -108,6 +108,7 @@ export default class ChatLine extends Module { if ( data.tokenize ) { const tokens = data.ffz_tokens = data.ffz_tokens || this.chat.tokenizeMessage({ + badges: {}, message: text, id: msg.id, user: msg.user, diff --git a/src/utilities/object.ts b/src/utilities/object.ts index 8865d7aa..6e3dfa4c 100644 --- a/src/utilities/object.ts +++ b/src/utilities/object.ts @@ -64,7 +64,7 @@ export function isValidShortcut(key: string) { */ export const generateUUID = crypto.randomUUID ? () => crypto.randomUUID() - : function generateUUID(input?: any) { + : function generateUUID(input?: any): string { return input // if the placeholder was passed, return ? ( // a random number from 0 to 15 input ^ // unless b is 8,