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;