From 0ffc041c0ce3f45734d6f61c7950b91a1da71a1d Mon Sep 17 00:00:00 2001 From: SirStendec Date: Fri, 20 Sep 2024 13:30:11 -0400 Subject: [PATCH] 4.74.0 * Added: Support for Twitch's new Shared Chat feature. When a Shared Chat is set up, FrankerFaceZ will let you use emotes from any of the channels in the Shared Chat. Additionally, there are minor changes to how chat lines are rendered in line with the native Twitch experience to display which channel a given message originated in. * Fixed: Issue where FrankerFaceZ was not correctly locating some Twitch web app internals. * Fixed: Issue where your bits balance would display on the Power-ups and Rewards button despite having the option to hide the bits UI enabled. (Closes #1543) * Fixed: Avatars still having some rounding when using the setting to disable rounding. (Closes #1540) * Fixed: Issue where `site.chat`:`addNotice()` could throw an error if calling it before we have extracted the current chat types from the Twitch web app. --- changelog.html | 713 ------------------ package.json | 2 +- src/modules/chat/badges.jsx | 24 +- src/modules/chat/emotes.js | 72 +- src/modules/chat/index.js | 4 +- src/modules/chat/room.js | 50 +- src/sites/twitch-twilight/index.js | 2 + .../modules/chat/emote_menu.jsx | 45 +- .../twitch-twilight/modules/chat/index.js | 200 ++++- .../twitch-twilight/modules/chat/line.js | 85 ++- .../css_tweaks/styles/square-avatars.scss | 3 +- src/utilities/constants.ts | 4 +- styles/chat.scss | 16 + 13 files changed, 445 insertions(+), 775 deletions(-) delete mode 100644 changelog.html diff --git a/changelog.html b/changelog.html deleted file mode 100644 index 3c8244a8..00000000 --- a/changelog.html +++ /dev/null @@ -1,713 +0,0 @@ -
4.0.0-rc5.1@5cda0595608c64d32de4
- - -
4.0.0-rc5@a36c49ab78f754fcd1c6
- - -
4.0.0-rc4.7@f9f030a275798072a22e
- - -
4.0.0-rc4.6@88d18379ce08c403e3f0
- - -
4.0.0-rc4.5@f47412afa7703e2d3b18
- - -
4.0.0-rc4.4@46f98c4cd4559eaa9828
- - -
4.0.0-rc4.3@46f98c4cd4559eaa9828
- - -
4.0.0-rc4.2@bfc82fd92a39871e75ff
- - -
4.0.0-rc4.1@3f78ab6b5e2898f3756f
- - -
4.0.0-rc4@012130d37baabae67622
- - -
4.0.0-rc3.4@104efaa42e14d30191d1
- - -
4.0.0-rc3.3@85ad2d458fb808c0365f
- - -
4.0.0-rc3.1@662c50c7e3e4ac110441
- - -
4.0.0-rc3@f48b64e778d576602925
- - -
4.0.0-rc2@377f701926189263186b
- - -
4.0.0-rc1.12@b04d3c600e5260fcd7cd
- - -
4.0.0-rc1.11@eed9eb9f5eb9acdb58ac
- - -
4.0.0-rc1.10@d27dc686044b45c844e4
- - -
4.0.0-rc1.9@d5a7ef61195e86dc7277
- - -
4.0.0-rc1.8@8254abfe4d4c824f58d6
- - -
4.0.0-rc1.7@498c1b079484a762958a
- - -
4.0.0-rc1.6@3d53d461b59654a0ec14
- - -
4.0.0-rc1.5@eb1433f63b4667bf9010
- - -
4.0.0-rc1.4@2009dc29d6bd5e122bd6
- - -
4.0.0-rc1.3
- - -
4.0.0-rc1@4a134ad5179b17981a92
- - -
4.0.0-beta2.18.2@a1a7fb774d62948bacc5
- - -
4.0.0-beta2.18.1@988ba86433ce4bfd636d
- - -
4.0.0-beta2.18@c8636911fc387a9f5e0c
- - -
4.0.0-beta2.17@dce1b0c5268bdd3fe086
- - -
4.0.0-beta2.16@9fac4d6cabd486c45f4e
- - -
4.0.0-beta2.15@61e6d676fdac89cf0592
- - -
4.0.0-beta2.14@d66f702097d2c0295697
- - -
4.0.0-beta2.13@64fec6b80d1f6a60c263
- - -
4.0.0-beta2.12@850fac83181587018cdb
- - -
4.0.0-beta2.11@850fac83181587018cdb
- - -
4.0.0-beta2.9@665575cf426293ec11da
- - -
4.0.0-beta2.6@b85fa005ec1f3929cdd8
- - -
4.0.0-beta2.5@b3fb24504616675ad2b9
- - -
4.0.0-beta2.4@b3fb24504616675ad2b9
- - -
4.0.0-beta2.3@a07fb33207e6659acc9f
- - -
4.0.0-beta2.2@201497e9898b452ba698
- - -
4.0.0-beta2.1@1a56c5fabae6fb37d845
- - -
4.0.0-beta2@65ca9bedbd1b59ec8df4
- - -
4.0.0-beta1.10@77498dc31e57b48d0549
- - -
4.0.0-beta1.9@b27c86408c133765e687
- - -
4.0.0-beta1.8@c07590bbb2a94b83c0e3
- - -
4.0.0-beta1.7@014a758f744a54c37b26
- - -
4.0.0-beta1.7@177445c5a2dd0b0b9857
- - -
4.0.0-beta1.7@a50bda0e204137eb8f28
- - -
4.0.0-beta1.7@18be0a772c267953f6e3
- - -
4.0.0-beta1.6@d2469b218214357ed0c0
- - -
4.0.0-beta1.6@e81b570ec2664e4cd19c
- - -
4.0.0-beta1.6@3402c0380be5b35d7f16
- - -
4.0.0-beta1.6@b26925b82613bdc459b5
- - -
4.0.0-beta1.6@66bf9e883f32aba529af
- - -
4.0.0-beta1.6@0a9fd7bd2f3805c7acc9
- - -
4.0.0-beta1.6@a5ecaae56ffd500ab715
- - -
4.0.0-beta1.6@c643fcdd1cb8343964c3
- - -
4.0.0-beta1.6@5442f1e095968e230f60
- - -
4.0.0-beta1.6@e9ed380d87da80b42502
- - -
4.0.0-beta1.6@1d563b0f42b9912f8494
- - -
4.0.0-beta1.5@08fecb05d7e54dc038b0
- - -
4.0.0-beta1.5@e5095bc624a4d0c6be15
- - -
4.0.0-beta1.5@ef163f5d644217193110
- - -
4.0.0-beta1.5@a752805865b1313466a7
- - -
4.0.0-beta1.5@676b97e4ce5229b90db5
- - -
4.0.0-beta1.5@bd87103fc1c64cf0df6d
- - -
4.0.0-beta1.5@bd87103fc1c64cf0df6d
- - -
4.0.0-beta1.5@a072b3e2b1e9dd395378
- - -
4.0.0-beta1.5@ab19f207a73078a1e97f
- - -
4.0.0-beta1.5@5f6029b8672c05bfed85
- - -
4.0.0-beta1.5@5aed9c5b086948ebcfc3
- - -
4.0.0-beta1.5@68369664d8835665997c
- - -
4.0.0-beta1.5@9a60ce1ee6c58905c6eb
- - -
4.0.0-beta1.5@764c7c372c158220ed04
- - -
4.0.0-beta1.5@fbebd8ab68081089f9f4
- - -
4.0.0-beta1.5@88b2aa86d34d0649d0d5
- - -
4.0.0-beta1.4@317c31074720f7071bd3
- - -
4.0.0-beta1.4@7ca245f1bf1509160a2c
- - - -
4.0.0-beta1.4@8e759e6ddfa7aa70cfea
- - -
4.0.0-beta1.4@eb51eeb2dadafdea2bde
- - -
4.0.0-beta1.3@5e6a1dc050c847836d76
- - -
4.0.0-beta1.3@e9f50a51ebfc3148faf9
- - -
4.0.0-beta1.3@af36d7201d43cc346d1b
- - -
4.0.0-beta1.3@7493d51cfb8e1b4448f0
- - -
4.0.0-beta1.3@da5b35d5323e5151e3ea
- - -
4.0.0-beta1.3@1ee69894e169e3173e19
- - -
4.0.0-beta1.3@f93396a7a97f1a01b284
- - -
4.0.0-beta1.3@e82e3deb4ad3e3f1b253
- - -
4.0.0-beta1.3@0c55f4f15b6397d644f4
- - -
4.0.0-beta1.3@fb9568932222b25ebd88
- - -
4.0.0-beta1.3@c89ba74a13dda449dfec
- - -
4.0.0-beta1.3@bdf2ec93761131252233
- - -
4.0.0-beta1.3@3feb3c14c65d7ebbed91
- - -
4.0.0-beta1.3@f855394a01c28a59836c
- - -
4.0.0-beta1.3@e9879a42770421353fc3
- - -
4.0.0-beta1.3@e1583f75d1ada7e37ca8
- - -
4.0.0-beta1.3@bebd15e93d1888192504
- - -
4.0.0-beta1.3@63206f4ff1c95b591873
- - -
4.0.0-beta1.3@b86de82715b14711a01c
- - -
4.0.0-beta1.2@5ddb43a7481741a4fdec
- - -
4.0.0-beta1.2@843d3308f09fa8d42276
- - -
4.0.0-beta1.2@a2ef3cb4f248129b2a3b
- - -
4.0.0-beta1
- - -
View Older
-
\ No newline at end of file diff --git a/package.json b/package.json index 6b676125..73eedddf 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.73.1", + "version": "4.74.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "private": true, "license": "Apache-2.0", diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index de888f75..75701844 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -542,6 +542,14 @@ export default class Badges extends Module { let promises = false; for(const d of ds.data) { + let source_channel = null; + if ( d.room ) { + const source = this.resolve('site.chat')?.shared_room_data?.get(d.room); + source_channel = source?.displayName; + if (source_channel) + source_channel = `\n\n${source_channel}`; + } + const p = d.provider; if ( p === 'twitch' ) { const bd = this.getTwitchBadge(d.badge, d.version, ds.room_id, ds.room_login), @@ -575,7 +583,7 @@ export default class Badges extends Module { out.push(
{show_previews && } - {title} + {title}{source_channel}
); } else if ( p === 'ffz' ) { @@ -593,7 +601,7 @@ export default class Badges extends Module { backgroundImage: `url("${d.image}")` }} />} - {d.title}{stuff||''} + {d.title}{stuff||''}{source_channel} ))); } else @@ -605,7 +613,7 @@ export default class Badges extends Module { backgroundImage: `url("${d.image}")` }} />} - {d.title}{extra||''} + {d.title}{extra||''}{source_channel} ); } } @@ -832,8 +840,8 @@ export default class Badges extends Module { //user = msg.user || {}, //user_id = user.id, //user_login = user.login, - room_id = msg.roomID, - room_login = msg.roomLogin, + room_id = msg.sourceRoomID ? msg.sourceRoomID : msg.roomID, + room_login = msg.sourceRoomID ? null : msg.roomLogin, room = this.parent.getRoom(room_id, room_login, true), badges = msg.ffz_badges; // this.getBadges(user_id, user_login, room_id, room_login); @@ -867,6 +875,7 @@ export default class Badges extends Module { image: mod_urls[4] || mod_urls[2] || mod_urls[1], color: '#34ae0a', title: bd ? bd.title : 'Moderator', + room: room_id, data }); @@ -877,6 +886,7 @@ export default class Badges extends Module { image: vip_urls[4] || vip_urls[2] || vip_urls[1], color: 'transparent', title: bd ? bd.title : 'VIP', + room: room_id, data }); @@ -885,6 +895,7 @@ export default class Badges extends Module { provider: 'twitch', badge: badge_id, version, + room: room_id, data }); @@ -934,6 +945,7 @@ export default class Badges extends Module { provider: 'ffz', id: badge.id, badge, + room: badge.room, image: bu[4] || bu[2] || bu[1], color: badge.color || full_badge.color, title: badge.title || full_badge.title, @@ -1106,7 +1118,7 @@ export default class Badges extends Module { const room_user = room && room.getUser(user_id, user_login, true); const out = (global_user?.badges ? global_user.badges._cache : []).concat( - room_user?.badges ? room_user.badges._cache : []); + room_user?.badges ? room_user.badges._cache.map(x => ({...x, room: room_id})) : []); if ( this.bulk.size ) { const str_user = String(user_id); diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 40a4668f..0f2f1d28 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -1586,7 +1586,7 @@ export default class Emotes extends Module { room_user = room && room.getUser(user_id, user_login, true), user = this.parent.getUser(user_id, user_login, true); - const out = (user?.emote_sets ? user.emote_sets._cache : []).concat( + let out = (user?.emote_sets ? user.emote_sets._cache : []).concat( room_user?.emote_sets ? room_user.emote_sets._cache : [], room?.emote_sets ? room.emote_sets._cache : [], this.default_sets._cache @@ -1600,6 +1600,19 @@ export default class Emotes extends Module { } } + // Shared Chats + if ( room?.shared_chats?.size > 0 ) + for(const shared_id of room.shared_chats) { + const shared_room = this.parent.getRoom(shared_id, null, true), + shared_user = shared_room && shared_room.getUser(user_id, user_login, true); + + if ( shared_user?.emote_sets?._cache ) + out = out.concat(shared_user.emote_sets._cache); + + if ( shared_room?.emote_sets?._cache ) + out = out.concat(shared_room.emote_sets._cache); + } + return out; } @@ -1608,14 +1621,14 @@ export default class Emotes extends Module { .map(set_id => this.emote_sets[set_id]); } - _withSources(out, seen, emote_sets) { // eslint-disable-line class-methods-use-this + _withSources(out, seen, emote_sets, room_id = null) { // eslint-disable-line class-methods-use-this if ( ! emote_sets?._sources ) return; for(const [provider, data] of emote_sets._sources) for(const item of data) if ( ! seen.has(item) ) { - out.push([item, provider]); + out.push([item, provider, room_id]); seen.add(item); } @@ -1633,30 +1646,61 @@ export default class Emotes extends Module { this._withSources(out, seen, room.emote_sets); if ( room_user ) - this._withSources(out, seen, room_user); + this._withSources(out, seen, room_user.emote_sets); + + // Shared Chats + if ( room?.shared_chats?.size > 0 ) + for(const shared_id of room.shared_chats) { + const shared_room = this.parent.getRoom(shared_id, null, true), + shared_user = shared_room && shared_room.getUser(user_id, user_login, true); + + if ( shared_user ) + this._withSources(out, seen, shared_user.emote_sets, shared_id); + + if ( shared_room ) + this._withSources(out, seen, shared_room.emote_sets, shared_id); + } return out; } getRoomSetsWithSources(user_id, user_login, room_id, room_login) { return this.getRoomSetIDsWithSources(user_id, user_login, room_id, room_login) - .map(([set_id, source]) => [this.emote_sets[set_id], source]); + .map(([set_id, source, r_id]) => [this.emote_sets[set_id], source, r_id]); } getRoomSetIDs(user_id, user_login, room_id, room_login) { const room = this.parent.getRoom(room_id, room_login, true), room_user = room && room.getUser(user_id, user_login, true); - if ( ! room ) - return []; + let out; - if ( ! room_user?.emote_sets ) - return room.emote_sets ? room.emote_sets._cache : []; + if ( ! room ) + out = []; + + else if ( ! room_user?.emote_sets ) + out = room.emote_sets ? room.emote_sets._cache : []; else if ( ! room.emote_sets ) - return room_user.emote_sets._cache; + out = room_user.emote_sets._cache; - return room_user.emote_sets._cache.concat(room.emote_sets._cache); + else + out = room_user.emote_sets._cache.concat(room.emote_sets._cache); + + // Shared Chats + if ( room?.shared_chats?.size > 0 ) + for(const shared_id of room.shared_chats) { + const shared_room = this.parent.getRoom(shared_id, null, true), + shared_user = shared_room && shared_room.getUser(user_id, user_login, true); + + if ( shared_user?.emote_sets?._cache ) + out = out.concat(shared_user.emote_sets._cache); + + if ( shared_room?.emote_sets?._cache ) + out = out.concat(shared_room.emote_sets._cache); + } + + return out; } getRoomSets(user_id, user_login, room_id, room_login) { @@ -1678,7 +1722,7 @@ export default class Emotes extends Module { if ( ! seen.has(set_id) && users?._cache.has(str_user) ) { for(const [provider, data] of users._sources) { if ( data && data.includes(str_user) ) { - out.push([set_id, provider]); + out.push([set_id, provider, null]); break; } } @@ -1691,7 +1735,7 @@ export default class Emotes extends Module { getGlobalSetsWithSources(user_id, user_login) { return this.getGlobalSetIDsWithSources(user_id, user_login) - .map(([set_id, source]) => [this.emote_sets[set_id], source]); + .map(([set_id, source]) => [this.emote_sets[set_id], source, null]); } @@ -1705,7 +1749,7 @@ export default class Emotes extends Module { getSubSetsWithSources() { return this.getSubSetIDsWithSources() - .map(([set_id, source]) => [this.emote_sets[set_id], source]); + .map(([set_id, source]) => [this.emote_sets[set_id], source, null]); } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 2b8c525e..1598645d 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -2095,7 +2095,9 @@ export default class Chat extends Module { msg.deleted = !!msg.deletedAt; // Addon Badges - msg.ffz_badges = this.badges.getBadges(user.id, user.login, msg.roomID, msg.roomLogin); + msg.ffz_badges = msg.sourceRoomID + ? this.badges.getBadges(user.id, user.login, msg.sourceRoomID, null) + : this.badges.getBadges(user.id, user.login, msg.roomID, msg.roomLogin); return msg; } diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index fd4460f1..f15497e1 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -24,6 +24,7 @@ export default class Room { this.badges = null; this.users = {}; this.user_ids = {}; + this.shared_chats = new Set; this.manager = manager; this._id = id; @@ -60,6 +61,7 @@ export default class Room { user.destroy(); } + this.shared_chats = null; this.refs = null; this.users = null; this.user_ids = null; @@ -91,7 +93,7 @@ export default class Room { this.manager.room_ids[this._id] = null; } - +0 merge(other) { if ( ! this.login && other.login ) this.login = other.login; @@ -102,6 +104,10 @@ export default class Room { for(const ref of other.refs) this.ref(ref); + if ( other.shared_chats ) + for(const room of other.shared_chats ) + this.shareChat(room); + if ( other.emote_sets && other.emote_sets._sources ) { for(const [provider, sets] of other.emote_sets._sources.entries()) { for(const set_id of sets) @@ -150,6 +156,26 @@ export default class Room { } + + // TODO: Logic? + + shareChat(room) { + this.shared_chats.add(room); + } + + shareChats(rooms) { + this.shared_chats = new Set(rooms || []); + } + + unshareChat(room) { + if (room) + this.shared_chats.delete(room); + else + this.shared_chats.clear(); + } + + + _unloadAddon(addon_id) { // TODO: This return 0; @@ -473,9 +499,25 @@ export default class Room { updateBadges(badges) { this.badge_count = 0; - if ( ! Array.isArray(badges) ) + if ( badges instanceof Map ) { + const b = {}; + for(const [sid, variants] of badges.entries()) { + const bs = b[sid] = b[sid] || { + __cat: getBadgeCategory(sid) + }; + + for(const [version, data] of variants.entries()) { + bs[version] = fixBadgeData(data); + this.badge_count++; + } + } + + this.badges = b; + + } else if ( ! Array.isArray(badges) ) { this.badges = badges; - else { + + } else { // Rooms can have no badges, so we want to allow that. const b = {}; for(const data of badges) { @@ -660,4 +702,4 @@ export default class Room { this.style.set('bits', out.join('\n')); } -} \ No newline at end of file +} diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 957fec67..e2903dab 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -206,6 +206,8 @@ Twilight.KNOWN_MODULES = { simplebar: n => n.globalObserver && n.initDOMLoadedElements, react: n => n.Component && n.createElement, core: n => { + if ( n.x2?.experiments && n.x2.intl ) + return n.x2; if ( n['$6']?.experiments ) return n['$6']; if ( n.p?.experiments ) diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 61daa851..3182d568 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -863,12 +863,13 @@ export default class EmoteMenu extends Module { />)}
- {visibility ? - (hidden ? - t.i18n.t('emote-menu.visibility.hidden', 'Hidden') : - t.i18n.t('emote-menu.visibility.visible', 'Visible') ) - : source - } + { + visibility ? + (hidden ? + t.i18n.t('emote-menu.visibility.hidden', 'Hidden') : + t.i18n.t('emote-menu.visibility.visible', 'Visible') ) + : source + } {(visibility ? false : filtered) ? '' :
} ) : null} {collapsed || this.renderBody(show_heading)} @@ -2362,12 +2363,12 @@ export default class EmoteMenu extends Module { let grouped_sets = {}; - for(const [emote_set, provider] of ffz_room) { + for(const [emote_set, provider, source_id] of ffz_room) { if ( seen_sets.has(emote_set) ) continue; seen_sets.add(emote_set); - const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets); + const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, false, undefined, source_id); if ( section ) { section.emotes.sort(sort_emotes); @@ -2465,10 +2466,16 @@ export default class EmoteMenu extends Module { } - processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked = false, state) { // eslint-disable-line class-methods-use-this + processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked = false, state, source_id) { // eslint-disable-line class-methods-use-this if ( ! emote_set || ! emote_set.emotes ) return null; + let source_name; + if ( source_id ) { + const room = t.parent.shared_room_data?.get(source_id); + source_name = room?.displayName ?? room?.login; + } + const fav_key = emote_set.source || 'ffz', known_favs = t.emotes.getFavorites(fav_key), known_hidden = t.emotes.getHidden(fav_key), @@ -2482,13 +2489,21 @@ export default class EmoteMenu extends Module { pdata.name) : emote_set.source || 'FFZ', - title = provider === 'main' ? - t.i18n.t('emote-menu.main-set', 'Channel Emotes') : - (emote_set.title || t.i18n.t('emote-menu.unknown-set', `Set #{set_id}`, {set_id: emote_set.id})); + title = provider === 'main' + ? source_name + ? t.i18n.t('emote-menu.source-set', '{channel}\'s Emotes', {channel: source_name}) + : t.i18n.t('emote-menu.main-set', 'Channel Emotes') + : (emote_set.title || t.i18n.t('emote-menu.unknown-set', `Set #{set_id}`, {set_id: emote_set.id})); let sort_key = pdata && pdata.sort_key || emote_set.sort; if ( sort_key == null ) - sort_key = emote_set.title.toLowerCase().includes('global') ? 100 : 0; + sort_key = emote_set.title.toLowerCase().includes('global') + ? 100 + : 0; + + // Shared Chat emote sets always come after. + if (source_id) + sort_key += 50; let section, emotes, locks; @@ -2502,6 +2517,7 @@ export default class EmoteMenu extends Module { image: emote_set.icon, title, source, + channel_source: source_name, force_global: emote_set.force_global }); @@ -2514,6 +2530,7 @@ export default class EmoteMenu extends Module { icon: 'zreknarf', title, source, + channel_source: source_name, emotes, force_global: emote_set.force_global, all_locked: true @@ -2781,7 +2798,7 @@ export default class EmoteMenu extends Module { key={data.key} class={`${active ? 'emote-picker-tab-item-wrapper__active ' : ''}${padding ? 'tw-mg-y-05' : 'tw-mg-y-1'} tw-c-text-inherit tw-interactable ffz-interactive ffz-interactable--hover-enabled ffz-interactable--default tw-block tw-full-width ffz-tooltip ffz-tooltip--no-mouse`} data-key={data.key} - data-title={`${data.i18n ? t.i18n.t(data.i18n, data.title) : data.title}\n${data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source}`} + data-title={`${data.i18n ? t.i18n.t(data.i18n, data.title) : data.title}\n${data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source}${data.channel_source ? ` (${data.channel_source})` : ''}`} data-tooltip-side="left" onClick={this.clickSideNav} > diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index e1146303..6be03bb8 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -166,6 +166,9 @@ export default class ChatHook extends Module { this.should_enable = true; + this.shared_room_data = new Map; + this.shared_rooms = {}; + this.colors = new ColorAdjuster; this.inverse_colors = new ColorAdjuster; @@ -213,6 +216,12 @@ export default class ChatHook extends Module { Twilight.CHAT_ROUTES ); + this.ChatRenderer = this.fine.define( + 'chat-renderer', + n => n.mapMessageToChatLine && n.reportChatRenderSent, + Twilight.CHAT_ROUTES + ); + this.ChatContainer = this.fine.define( 'chat-container', n => n.closeViewersList && n.onChatInputFocus, @@ -490,6 +499,15 @@ export default class ChatHook extends Module { } });*/ + this.settings.add('chat.banners.shared-chat', { + default: true, + ui: { + path: 'Chat > Shared Chat >> Behavior', + title: 'Allow the Shared Chat notice to be displayed in chat when a Shared Chat is enabled.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.banners.pinned-message', { default: true, ui: { @@ -611,6 +629,31 @@ export default class ChatHook extends Module { } }); + this.settings.add('chat.shared-chat.style', { + default: null, + ui: { + path: 'Chat > Shared Chat >> Appearance', + title: 'Pill Style', + component: 'setting-select-box', + description: 'This controls the appearance of the pill at the left-side of chat messages when a message is part of a Shared Chat. By default, this is Avatar for moderators and broadcasters and Hidden for everyone else.', + data: [ + {value: null, title: 'Automatic'}, + {value: 0, title: 'Hidden'}, + {value: 1, title: 'Channel Name'}, + {value: 2, title: 'Avatar'} + ] + } + }); + + this.settings.add('chat.shared-chat.username-tooltip', { + default: true, + ui: { + path: 'Chat > Shared Chat >> Appearance', + component: 'setting-check-box', + title: 'Display the source channel of a chat message when hovering over the poster\'s username.', + } + }); + this.settings.add('chat.width', { default: null, ui: { @@ -1064,6 +1107,9 @@ export default class ChatHook extends Module { this.PointsButton.forceUpdate(); this.PointsClaimButton.forceUpdate(); }); + this.chat.context.on('changed:chat.bits.show', () => { + this.PointsButton.forceUpdate(); + }); this.chat.context.on('changed:chat.banners.hype-train', this.cleanHighlights, this); this.chat.context.on('changed:chat.subs.gift-banner', this.cleanHighlights, this); @@ -1072,6 +1118,7 @@ export default class ChatHook extends Module { this.chat.context.on('changed:chat.banners.drops', this.cleanHighlights, this); this.chat.context.on('changed:chat.banners.pinned-message', this.cleanHighlights, this); this.chat.context.on('changed:chat.banners.hide-appleplus', this.cleanHighlights, this); + this.chat.context.on('changed:chat.banners.shared-chat', this.cleanHighlights, this); this.chat.context.on('changed:chat.disable-handling', this.updateDisableHandling, this); @@ -1368,13 +1415,20 @@ export default class ChatHook extends Module { if ( ! t.chat.context.get('chat.points.show-button') ) return null; - if ( ! t.chat.context.get('chat.points.show-rewards') ) { - const aq = this.state.animationQueue; + const old_aq = this.state.animationQueue, + old_bits = this.props.bitsEnabled; + + if ( ! t.chat.context.get('chat.points.show-rewards') ) this.state.animationQueue = []; - const out = old_render.call(this); - this.state.animationQueue = aq; - return out; - } + + if ( ! t.chat.context.get('chat.bits.show') ) + this.props.bitsEnabled = false; + + const out = old_render.call(this); + + this.state.animationQueue = old_aq; + this.props.bitsEnabled = old_bits; + return out; } catch(err) { t.log.capture(err); @@ -1495,7 +1549,7 @@ export default class ChatHook extends Module { this.ChatBufferConnector.ready((cls, instances) => { for(const inst of instances) this.connectorMounted(inst); - }) + }); this.ChatController.ready((cls, instances) => { const t = this, @@ -1536,6 +1590,14 @@ export default class ChatHook extends Module { this.chatMounted(inst); }); + this.ChatRenderer.on('mount', this.rendererMounted, this); + this.ChatRenderer.on('unmount', this.rendererUnmounted, this); + this.ChatRenderer.on('update', this.rendererUpdated, this); + + this.ChatRenderer.ready((cls, instances) => { + for(const inst of instances) + this.rendererMounted(inst); + }) this.ChatContainer.on('mount', this.containerMounted, this); this.ChatContainer.on('unmount', this.containerUnmounted, this); //removeRoom, this); @@ -1753,7 +1815,8 @@ export default class ChatHook extends Module { 'prediction': this.chat.context.get('chat.banners.prediction'), 'poll': this.chat.context.get('chat.banners.polls'), 'pinned_chat': this.chat.context.get('chat.banners.pinned-message'), - 'mw-drop-available': this.chat.context.get('chat.banners.drops') + 'mw-drop-available': this.chat.context.get('chat.banners.drops'), + 'shared_chat': this.chat.context.get('chat.banners.shared-chat') }; const highlights = this.community_stack?.highlights; @@ -2434,7 +2497,7 @@ export default class ChatHook extends Module { if ( room === '*' || inst.props.channelLogin.toLowerCase() === room ) { if ( typeof message === 'string' ) inst.addMessage({ - type: this.chat_types.Notice, + type: (this.chat_types ?? CHAT_TYPES).Notice, message }); else { @@ -2449,7 +2512,7 @@ export default class ChatHook extends Module { } inst.addMessage({ - type: this.chat_types.Message, + type: (this.chat_types ?? CHAT_TYPES).Message, channel: `#${login}`, roomID: id, roomLogin: login, @@ -2633,6 +2696,7 @@ export default class ChatHook extends Module { const out = i.convertMessage({message: e}); out.ffz_type = 'resub'; out.gift_theme = e.giftTheme; + out.sharedChat = e.sharedChat; out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null; out.sub_plan = e.methods; out.sub_multi = e.multiMonthData?.multiMonthDuration ? { @@ -2675,6 +2739,7 @@ export default class ChatHook extends Module { const out = i.convertMessage(e); out.ffz_type = 'hype'; + out.sharedChat = e.sharedChat; out.hype_amount = e.amount; out.hype_canonical_amount = e.canonical_amount; out.hype_currency = e.currency; @@ -2715,6 +2780,7 @@ export default class ChatHook extends Module { const out = i.convertMessage({message: e}); out.ffz_type = 'resub'; + out.sharedChat = e.sharedChat; out.gift_theme = e.giftTheme; out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null; out.sub_cumulative = e.cumulativeMonths || 0; @@ -2774,6 +2840,7 @@ export default class ChatHook extends Module { e.body = ''; const out = i.convertMessage({message: e}); out.ffz_type = 'sub_gift'; + out.sharedChat = e.sharedChat; out.sub_recipient = { id: e.recipientID, login: e.recipientLogin, @@ -2802,6 +2869,7 @@ export default class ChatHook extends Module { if ( t.chat.context.get('chat.filtering.blocked-types').has('CommunityIntroduction') ) { const out = i.convertMessage(e); + out.sharedChat = e.sharedChat; return i.postMessageToCurrentChannel(e, out); } @@ -2848,6 +2916,7 @@ export default class ChatHook extends Module { e.body = ''; const out = i.convertMessage({message: e}); out.ffz_type = 'sub_gift'; + out.sharedChat = e.sharedChat; out.gift_theme = e.giftTheme; out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null; out.sub_anon = true; @@ -2891,6 +2960,7 @@ export default class ChatHook extends Module { e.body = ''; const out = i.convertMessage({message: e}); out.ffz_type = 'sub_mystery'; + out.sharedChat = e.sharedChat; out.gift_theme = e.giftTheme; out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null; out.mystery = mystery; @@ -2929,6 +2999,7 @@ export default class ChatHook extends Module { e.body = ''; const out = i.convertMessage({message: e}); out.ffz_type = 'sub_mystery'; + out.sharedChat = e.sharedChat; out.sub_anon = true; out.mystery = mystery; out.sub_plan = e.plan; @@ -2954,6 +3025,7 @@ export default class ChatHook extends Module { return old_ritual.call(i, e); const out = i.convertMessage(e); + out.sharedChat = e.sharedChat; out.ffz_type = 'ritual'; out.ritual = e.type; @@ -2987,10 +3059,12 @@ export default class ChatHook extends Module { reward: reward, message: out, userID: out.user.userID, - animationID: e.animationID + animationID: e.animationID, + sharedChat: e.sharedChat }) } else { out.ffz_animation_id = e.animationID; + out.sharedChat = e.sharedChat; out.ffz_type = 'points'; out.ffz_reward = reward; out.ffz_reward_highlight = isHighlightedReward(reward); @@ -3281,6 +3355,110 @@ export default class ChatHook extends Module { } + // ======================================================================== + // Chat Renderers + // ======================================================================== + + rendererMounted(cont, props) { + if ( ! props ) + props = cont.props; + + // We keep our own track of shared rooms, so that we can load/unload + // the associated data as relevant. + this.updateRendererSharedChats(cont, props?.sharedChatDataByChannelID); + } + + + updateRendererSharedChats(cont, data) { + data ??= cont.props.sharedChatDataByChannelID; + + if ( cont._ffz_cached_shared === data ) + return; + + this.shared_room_data = cont._ffz_cached_shared = data; + this.shared_rooms = cont._ffz_shared_rooms = cont._ffz_shared_rooms || {}; + + const unexpected = new Set(Object.keys(cont._ffz_shared_rooms)); + let badges_updated = false; + + if (data != null) + for(const [room_id, d2] of data.entries()) { + unexpected.delete(room_id); + + let room = cont._ffz_shared_rooms[room_id]; + if ( ! room ) { + // We need to add the room. + room = this.chat.getRoom(room_id, d2.login); + room.ref(cont); + cont._ffz_shared_rooms[room_id] = room; + } + + // Check badges. + const bd = d2.badges?.channelsBySet; + + // Since we don't have a flat structure, we have to recurse + // a little to get the count. + let count = 0; + if (bd) { + for(const entry of bd.values()) { + for(const _ of entry.values()) { + count++; + } + } + } + + if ( room.badgeCount() !== count ) { + badges_updated = true; + room.updateBadges(bd); + } + } + + // Unref rooms that are no longer shared. + for(const room_id of unexpected) { + const room = cont._ffz_shared_rooms[room_id]; + if ( room ) + room.unref(cont); + + room.unshareChat(); + delete cont._ffz_shared_rooms[room_id]; + } + + const shared = Object.keys(cont._ffz_shared_rooms); + + for(const room of Object.values(cont._ffz_shared_rooms)) + room.shareChats(shared); + + //this.log.info('!!!!! updated shared chats', cont._ffz_shared_rooms); + + if ( shared.length > 0 && this.settings.provider.get('shared-chat-notice') !== 1 ) { + this.settings.provider.set('shared-chat-notice', 1); + this.addNotice('*', { + icon: 'ffz-i-zreknarf', + message: this.i18n.t('chat.shared-chat.welcome', 'FrankerFaceZ: This is a Shared Chat, a new feature from Twitch to combine multiple stream chats into one. FrankerFaceZ support is early, so some messages might not look correct. Sorry for any trouble!') + }); + } + + if ( badges_updated ) + this.chat_line.updateLineBadges(); + } + + rendererUnmounted(cont) { + // Unref all shared rooms. + if (cont._ffz_shared_rooms) + for(const room of Object.values(cont._ffz_shared_rooms)) + room.unref(cont); + + cont._ffz_shared_rooms = null; + cont._ffz_cached_shared = null; + this.shared_room_data = new Map; + this.shared_rooms = {}; + } + + rendererUpdated(cont, props) { + this.updateRendererSharedChats(cont, props?.sharedChatDataByChannelID); + } + + // ======================================================================== // Chat Containers // ======================================================================== diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 886cd926..ded22c60 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -819,6 +819,42 @@ export default class ChatLine extends Module { ]); } + cls.prototype.ffzRenderSharedChatPill = function() { + let style = t.chat.context.get('chat.shared-chat.style'); + if ( style == null ) + style = this.props.isCurrentUserModerator ? 2 : 0; + if ( style === 0 ) + return null; + + const msg = this.props.message, + source_id = msg.sourceRoomID, + source = source_id && this.props.sharedChatDataByChannelID?.get(source_id), + in_source = ! source || source_id === (msg.roomId ?? this.props.channelID); + + if ( ! source ) + return null; + + const title = t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login}); + + if ( style === 1 ) + return e('span', { + className: `ffz-pill ffz-tooltip tw-mg-r-05 ${in_source ? 'ffz-pill--brand' : ''}`, + 'data-title': title + }, `${source.displayName ?? source.login}`); + + if ( style === 2 ) + return e('figure', { + className: `ffz-tooltip ffz-tooltip--no-mouse ffz-shared-chat-badge ffz-avatar tw-border-radius-rounded ${in_source ? 'ffz-shared-chat-badge--active' : ''} tw-mg-r-05`, + 'data-title': t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName}), + }, e('img', { + className: 'tw-block tw-border-radius-rounded tw-image', + src: source.profileImageURL, + alt: source.displayName + })); + + return null; + } + cls.prototype.ffzNewRender = function() { try { this._ffz_no_scan = true; @@ -852,6 +888,10 @@ other {# messages were deleted by a moderator.} ); } + // First, we need to handle Shared Chat. + const source_id = msg.sourceRoomID, + source = source_id && this.props.sharedChatDataByChannelID?.get(source_id); + // Get the current room id and login. We might need to look these up. let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined, room_id = msg.roomId ? msg.roomId : this.props.channelID; @@ -872,6 +912,11 @@ other {# messages were deleted by a moderator.} const current_user = t.site.getUser(), current_room = {id: room_id, login: room}; + const is_in_source = ! source || source_id === room_id, + source_room = is_in_source + ? current_room + : {id: source_id, login: source.login}; + const reply_mode = t.chat.context.get('chat.replies.style'), has_replies = this.props && !!(this.props.hasReply || this.props.reply || ! this.props.replyRestrictedReason), can_replies = has_replies && msg.message && ! msg.deleted && ! this.props.disableReplyClick, @@ -909,7 +954,8 @@ other {# messages were deleted by a moderator.} event, message: msg, user: target_user, - room: current_room + room: current_room, + source: source_room }); t.emit('chat:user-click', fe); @@ -1088,8 +1134,10 @@ other {# messages were deleted by a moderator.} else if ( Array.isArray(user_class) ) user_class = user_class.join(' '); + const want_source_tip = source && t.chat.context.get('chat.shared-chat.username-tooltip'); + const user_props = { - className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${user_class ?? ''}`, + className: `chat-line__username notranslate${override_name ? ' ffz--name-override' : ''}${(override_name || want_source_tip) ? ' tw-relative ffz-il-tooltip__container' : ''} ${user_class ?? ''}`, role: 'button', style: { color }, onClick: this.ffz_user_click_handler, @@ -1111,10 +1159,25 @@ other {# messages were deleted by a moderator.} className: 'chat-author__display-name' }, override_name), e('div', { - className: 'ffz-il-tooltip ffz-il-tooldip--down ffz-il-tooltip--align-center' - }, username) + className: 'ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center' + }, [ + username, + want_source_tip + ? e('div', { + className: 'tw-font-size-8 tw-mg-t-05' + }, t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login})) + : null + ] + ) ] - : username + : want_source_tip + ? [ + username, + e('span', { + className: 'ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center' + }, t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login})) + ] + : username ); // The timestamp. @@ -1139,6 +1202,8 @@ other {# messages were deleted by a moderator.} message = [ twitch_reply, + source ? this.ffzRenderSharedChatPill() : null, + // The preamble timestamp, msg.ffz_no_actions ? null : t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this), @@ -1213,6 +1278,7 @@ other {# messages were deleted by a moderator.} if ( is_raw ) { notice.ffz_target.unshift( + source ? this.ffzRenderSharedChatPill() : null, notice.ffz_icon ?? null, timestamp, actions, @@ -1221,6 +1287,7 @@ other {# messages were deleted by a moderator.} } else notice = [ + source ? this.ffzRenderSharedChatPill() : null, notice.ffz_icon ?? null, timestamp, actions, @@ -1243,8 +1310,8 @@ other {# messages were deleted by a moderator.} 'div', { className: 'chat-line--inline chat-line__message', - 'data-room-id': msg.roomId ?? current_room.id, - 'data-room': msg.roomLogin, + 'data-room-id': source_room?.id ?? msg.roomId ?? current_room.id, + 'data-room': source_room?.login ?? msg.roomLogin, 'data-user-id': user?.userID, 'data-user': user?.lowerLogin, }, @@ -1305,8 +1372,8 @@ other {# messages were deleted by a moderator.} return e('div', { className: `${klass}${deleted ? ' ffz--deleted-message' : ''}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`, style: {backgroundColor: bg_css}, - 'data-room-id': msg.roomId ?? current_room.id, - 'data-room': msg.roomLogin, + 'data-room-id': source_room?.id ?? msg.roomId ?? current_room.id, + 'data-room': source_room?.login ?? msg.roomLogin, 'data-user-id': user?.userID, 'data-user': user?.lowerLogin, onMouseOver: anim_hover ? t.chat.emotes.animHover : null, diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss index 382c4968..bfb72881 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/square-avatars.scss @@ -6,6 +6,7 @@ --border-radius-rounded: 0 !important; } +a > [class*="intermediateHalo-"], .tw-image-avatar, .user-avatar-card__halo, .player-streaminfo__picture img[src], @@ -20,4 +21,4 @@ .user-avatar-animated .user-avatar-animated__halo { visibility: hidden; -} \ No newline at end of file +} diff --git a/src/utilities/constants.ts b/src/utilities/constants.ts index 08afe7f9..2c8b891f 100644 --- a/src/utilities/constants.ts +++ b/src/utilities/constants.ts @@ -136,7 +136,9 @@ export const RERENDER_SETTINGS = [ 'chat.filtering.hidden-tokens', 'chat.hype.message-style', 'chat.filtering.show-reasons', - 'chat.emotes.allow-gigantify' + 'chat.emotes.allow-gigantify', + 'chat.shared-chat.username-tooltip', + 'chat.shared-chat.style' ] as const; /** diff --git a/styles/chat.scss b/styles/chat.scss index 00fcae52..38d8c2b0 100644 --- a/styles/chat.scss +++ b/styles/chat.scss @@ -450,6 +450,22 @@ } +.ffz-shared-chat-badge { + display: inline-block; + height: 2.2rem; + width: 2.2rem; + margin: 0 .3rem .2rem 0; + padding: 0.2rem; + vertical-align: middle; + border: 2px solid transparent; + + &--active { + border-color: var(--color-border-brand); + } + +} + + .ffz-badge { display: inline-block; min-width: 1.8rem;