mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
Emote Menu? Emote Menu.
* Add the emote menu. * Add an option to replace the emote menu icon with the FFZ icon. * Add icons to the icon font. * Add a basic human duration formatting method to i18n. * Add methods to the emotes module to get sets including the providers. * Add a method to the emotes module to load an arbitrary set. * Add a map to the emotes module for identifying providers. * Add new events for when emote sets change. * Add support for loading featured channel emotes. * Add an option to suppress source sets in emote tooltips. * Add an option to display a sellout line in emote tooltips. * Remove emote menu from the WIP section of the home page of the menu. * Fix a typo in rich content. * Remove a bit of logging from fine. * Add helper methods for set comparison and basic debouncing to utilities/object. * Add constants for the emote menu. * Add methods to show/hide a tooltip to the tooltip data object.
This commit is contained in:
parent
92130ebac4
commit
e6e11fe562
23 changed files with 1423 additions and 26 deletions
|
@ -1,3 +1,18 @@
|
|||
<div class="list-header">4.0.0-beta2<span>@65ca9bedbd1b59ec8df4</span> <time datetime="2018-04-06">(2018-04-06)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Emote Menu</li>
|
||||
<li>Yes, really. You can stop asking now or saying it's broken.</li>
|
||||
<li> </li>
|
||||
<li>Fixed: Possible bugs when trying to rebuild CSS for a room that has been unloaded.</li>
|
||||
<li>Fixed: Typo in rich content hosts preventing links from opening without referral information.</li>
|
||||
<li>Fixed: Memory leak with tooltips.</li>
|
||||
<li>Changed: Added several more icons to the icon font.</li>
|
||||
<li>Changed: Add support for hiding emote sources and displaying an additional message to emote tooltips.</li>
|
||||
<li>API Added: Methods to get available emote sets along with the provider that added them.</li>
|
||||
<li>API Added: Events when available emote sets changed.</li>
|
||||
<li>API Added: Method to load an arbitrary emote set by ID.</li>
|
||||
</ul>
|
||||
|
||||
<div class="list-header">4.0.0-beta1.10<span>@77498dc31e57b48d0549</span> <time datetime="2018-04-03">(2018-04-03)</time></div>
|
||||
<ul class="chat-menu-content menu-side-padding">
|
||||
<li>Added: Rendering support for rich content blocks in chat.</li>
|
||||
|
|
Binary file not shown.
|
@ -64,6 +64,18 @@
|
|||
|
||||
<glyph glyph-name="help" unicode="" d="M500 82v107q0 8-5 13t-13 5h-107q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h107q8 0 13 5t5 13z m143 375q0 49-31 91t-77 65-95 23q-136 0-207-119-9-13 4-24l74-55q4-4 10-4 9 0 14 7 30 38 48 51 19 14 48 14 27 0 48-15t21-33q0-21-11-34t-38-25q-35-15-65-48t-29-70v-20q0-8 5-13t13-5h107q8 0 13 5t5 13q0 10 12 27t30 28q18 10 28 16t25 19 25 27 16 34 7 45z m214-107q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="calendar" unicode="" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="left-dir" unicode="" d="M357 600v-500q0-14-10-25t-26-11-25 11l-250 250q-10 11-10 25t10 25l250 250q11 11 25 11t26-11 10-25z" horiz-adv-x="357.1" />
|
||||
|
||||
<glyph glyph-name="inventory" unicode="" d="M444 406h167l0-239 111 39v200h111 56v-167l-22-56-423-166h-55v444l55-55z m-55 55v-444h-56l-311 166-22 56v167h56v-167l222-111 55 55v223l56 55z m444 56h-111l0 144-111 33v-177h-167l-55-56v333h55l423-111 22-55v-111h-56z m-833 0l0 111 22 55 311 111h56v-333l-56 56v216l-277-100v-116h-56z m83 150l-66 77 11 45 183 61 28-6 83-88-239-89z m6 83l22-33 128 44-28 33-122-44z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="lock" unicode="" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
|
||||
|
||||
<glyph glyph-name="lock-open" unicode="" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="arrows-cw" unicode="" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="twitter" unicode="" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="gauge" unicode="" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
|
||||
|
@ -74,6 +86,8 @@
|
|||
|
||||
<glyph glyph-name="keyboard" unicode="" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
|
||||
|
||||
<glyph glyph-name="calendar-empty" unicode="" d="M71-79h786v572h-786v-572z m215 679v161q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h36q8 0 13 5t5 13z m428 0v161q0 8-5 13t-13 5h-35q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="ellipsis-vert" unicode="" d="M214 154v-108q0-22-15-37t-38-16h-107q-23 0-38 16t-16 37v108q0 22 16 38t38 15h107q22 0 38-15t15-38z m0 285v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m0 286v-107q0-22-15-38t-38-16h-107q-23 0-38 16t-16 38v107q0 22 16 38t38 16h107q22 0 38-16t15-38z" horiz-adv-x="214.3" />
|
||||
|
||||
<glyph glyph-name="twitch" unicode="" d="M500 608v-242h-81v242h81z m222 0v-242h-81v242h81z m0-424l141 141v444h-666v-585h182v-121l121 121h222z m222 666v-565l-242-242h-182l-121-122h-121v122h-222v646l61 161h827z" horiz-adv-x="1000" />
|
||||
|
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
src/i18n.js
29
src/i18n.js
|
@ -84,6 +84,35 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
toHumanTime(duration, factor = 1) {
|
||||
// TODO: Make this better. Make all time handling better in fact.
|
||||
|
||||
duration = Math.floor(duration);
|
||||
|
||||
const years = Math.floor((duration * factor) / 31536000) / factor;
|
||||
if ( years >= 1 )
|
||||
return this.t('human-time.years', '%{count} year%{count|en_plural}', years);
|
||||
|
||||
const days = Math.floor((duration %= 31536000) / 86400);
|
||||
if ( days >= 1 )
|
||||
return this.t('human-time.days', '%{count} day%{count|en_plural}', days);
|
||||
|
||||
const hours = Math.floor((duration %= 86400) / 3600);
|
||||
if ( hours >= 1 )
|
||||
return this.t('human-time.hours', '%{count} hour%{count|en_plural}', hours);
|
||||
|
||||
const minutes = Math.floor((duration %= 3600) / 60);
|
||||
if ( minutes >= 1 )
|
||||
return this.t('human-time.minutes', '%{count} minute%{count|en_plural}', minutes);
|
||||
|
||||
const seconds = duration % 60;
|
||||
if ( seconds >= 1 )
|
||||
return this.t('human-time.seconds', '%{count} second%{count|en_plural}', seconds);
|
||||
|
||||
return this.t('human-time.none', 'less than a second');
|
||||
}
|
||||
|
||||
|
||||
async loadLocale(locale) {
|
||||
/*if ( locale === 'en' )
|
||||
return {};
|
||||
|
|
|
@ -95,7 +95,7 @@ class FrankerFaceZ extends Module {
|
|||
FrankerFaceZ.Logger = Logger;
|
||||
|
||||
const VER = FrankerFaceZ.version_info = {
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta1.10',
|
||||
major: 4, minor: 0, revision: 0, extra: '-beta2',
|
||||
build: __webpack_hash__,
|
||||
toString: () =>
|
||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||
|
|
|
@ -10,6 +10,8 @@ import {has, timeout, SourcedSet} from 'utilities/object';
|
|||
import {CLIENT_ID, API_SERVER} from 'utilities/constants';
|
||||
|
||||
|
||||
const EXTRA_INVENTORY = [33563];
|
||||
|
||||
const MODIFIERS = {
|
||||
59847: {
|
||||
modifier_offset: '0 15px 15px 0',
|
||||
|
@ -58,13 +60,21 @@ export default class Emotes extends Module {
|
|||
this.inject('socket');
|
||||
this.inject('settings');
|
||||
|
||||
this.twitch_inventory_sets = [];
|
||||
this.twitch_inventory_sets = new Set(EXTRA_INVENTORY);
|
||||
this.__twitch_emote_to_set = new Map;
|
||||
this.__twitch_set_to_channel = new Map;
|
||||
|
||||
this.default_sets = new SourcedSet;
|
||||
this.global_sets = new SourcedSet;
|
||||
|
||||
this.providers = new Map;
|
||||
|
||||
this.providers.set('featured', {
|
||||
name: 'Featured',
|
||||
i18n_key: 'emote-menu.featured',
|
||||
sort_key: 75
|
||||
})
|
||||
|
||||
this.emote_sets = {};
|
||||
this._set_refs = {};
|
||||
this._set_timers = {};
|
||||
|
@ -98,11 +108,40 @@ export default class Emotes extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
this.socket.on(':command:follow_sets', this.updateFollowSets, this);
|
||||
|
||||
this.loadGlobalSets();
|
||||
this.loadTwitchInventory();
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Featured Sets
|
||||
// ========================================================================
|
||||
|
||||
updateFollowSets(data) {
|
||||
for(const room_login in data)
|
||||
if ( has(data, room_login) ) {
|
||||
const room = this.parent.getRoom(null, room_login, true),
|
||||
new_sets = data[room_login] || [],
|
||||
emote_sets = room.emote_sets,
|
||||
providers = emote_sets._sources;
|
||||
|
||||
if ( providers && providers.has('featured') )
|
||||
for(const item of providers.get('featured'))
|
||||
if ( ! new_sets.includes(item) )
|
||||
room.removeSet('featured', item);
|
||||
|
||||
for(const set_id of new_sets) {
|
||||
room.addSet('featured', set_id);
|
||||
|
||||
if ( ! this.emote_sets[set_id] )
|
||||
this.loadSet(set_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Access
|
||||
// ========================================================================
|
||||
|
@ -124,6 +163,41 @@ 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
|
||||
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]);
|
||||
seen.add(item);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
getRoomSetIDsWithSources(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 [];
|
||||
|
||||
const out = [], seen = new Set;
|
||||
|
||||
this._withSources(out, seen, room.emote_sets);
|
||||
if ( room_user )
|
||||
this._withSources(out, seen, room_user);
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -142,6 +216,22 @@ export default class Emotes extends Module {
|
|||
.map(set_id => this.emote_sets[set_id]);
|
||||
}
|
||||
|
||||
getGlobalSetIDsWithSources(user_id, user_login) {
|
||||
const user = this.parent.getUser(user_id, user_login, true),
|
||||
out = [], seen = new Set;
|
||||
|
||||
this._withSources(out, seen, this.default_sets);
|
||||
if ( user )
|
||||
this._withSources(out, seen, user.emote_sets);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
getGlobalSetsWithSources(user_id, user_login) {
|
||||
return this.getGlobalSetIDsWithSources(user_id, user_login)
|
||||
.map(([set_id, source]) => [this.emote_sets[set_id], source]);
|
||||
}
|
||||
|
||||
getGlobalSetIDs(user_id, user_login) {
|
||||
const user = this.parent.getUser(user_id, user_login, true);
|
||||
if ( ! user )
|
||||
|
@ -171,28 +261,30 @@ export default class Emotes extends Module {
|
|||
// ========================================================================
|
||||
|
||||
addDefaultSet(provider, set_id, data) {
|
||||
const had_set = this.default_sets.includes(set_id);
|
||||
let changed = false;
|
||||
if ( ! this.default_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.default_sets.push(provider, set_id);
|
||||
this.refSet(set_id);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ( data )
|
||||
this.loadSetData(set_id, data);
|
||||
|
||||
if ( ! had_set )
|
||||
this.emit(':update-default-sets');
|
||||
if ( changed )
|
||||
this.emit(':update-default-sets', provider, set_id, true);
|
||||
}
|
||||
|
||||
removeDefaultSet(provider, set_id) {
|
||||
const had_set = this.default_sets.includes(set_id);
|
||||
let changed = false;
|
||||
if ( this.default_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.default_sets.remove(provider, set_id);
|
||||
this.unrefSet(set_id);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ( had_set && ! this.default_sets.includes(set_id) )
|
||||
this.emit(':update-default-sets');
|
||||
if ( changed )
|
||||
this.emit(':update-default-sets', provider, set_id, false);
|
||||
}
|
||||
|
||||
refSet(set_id) {
|
||||
|
@ -252,7 +344,41 @@ export default class Emotes extends Module {
|
|||
}
|
||||
|
||||
|
||||
loadSetUsers(data) {
|
||||
async loadSet(set_id, suppress_log = false, tries = 0) {
|
||||
let response, data;
|
||||
try {
|
||||
response = await fetch(`${API_SERVER}/v1/set/${set_id}`)
|
||||
} catch(err) {
|
||||
tries++;
|
||||
if ( tries < 10 )
|
||||
return setTimeout(() => this.loadGlobalSets(tries), 500 * tries);
|
||||
|
||||
this.log.error(`Error loading data for set "${set_id}".`, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! response.ok )
|
||||
return false;
|
||||
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch(err) {
|
||||
this.log.error(`Error parsing data for set "${set_id}".`, err);
|
||||
return false;
|
||||
}
|
||||
|
||||
const set = data.set;
|
||||
if ( set )
|
||||
this.loadSetData(set.id, set, suppress_log);
|
||||
|
||||
if ( data.users )
|
||||
this.loadSetUsers(data.users);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
loadSetUsers(data, suppress_log = false) {
|
||||
for(const set_id in data)
|
||||
if ( has(data, set_id) ) {
|
||||
const emote_set = this.emote_sets[set_id],
|
||||
|
@ -262,7 +388,8 @@ export default class Emotes extends Module {
|
|||
this.parent.getUser(undefined, login)
|
||||
.addSet('ffz-global', set_id);
|
||||
|
||||
this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`);
|
||||
if ( ! suppress_log )
|
||||
this.log.info(`Added "${emote_set ? emote_set.title : set_id}" emote set to ${users.length} users.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,6 +410,7 @@ export default class Emotes extends Module {
|
|||
new_ems = data.emotes = {},
|
||||
css = [];
|
||||
|
||||
data.id = set_id;
|
||||
data.emoticons = undefined;
|
||||
|
||||
for(const emote of ems) {
|
||||
|
@ -413,7 +541,11 @@ export default class Emotes extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
this.twitch_inventory_sets = data.emoticon_sets ? Object.keys(data.emoticon_sets) : [];
|
||||
const sets = this.twitch_inventory_sets = new Set(EXTRA_INVENTORY);
|
||||
for(const set in data.emoticon_sets)
|
||||
if ( has(data.emoticon_sets, set) )
|
||||
sets.add(parseInt(set, 10));
|
||||
|
||||
this.log.info('Twitch Inventory Sets:', this.twitch_inventory_sets);
|
||||
}
|
||||
|
||||
|
|
|
@ -240,19 +240,25 @@ export default class Room {
|
|||
// ========================================================================
|
||||
|
||||
addSet(provider, set_id, data) {
|
||||
let changed = false;
|
||||
if ( ! this.emote_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.emote_sets.push(provider, set_id);
|
||||
this.manager.emotes.refSet(set_id);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if ( data )
|
||||
this.manager.emotes.loadSetData(set_id, data);
|
||||
|
||||
if ( changed )
|
||||
this.manager.emotes.emit(':update-room-sets', this, provider, set_id, true);
|
||||
}
|
||||
|
||||
removeSet(provider, set_id) {
|
||||
if ( this.emote_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.emote_sets.remove(provider, set_id);
|
||||
this.manager.emotes.unrefSet(set_id);
|
||||
this.manager.emotes.emit(':update-room-sets', this, provider, set_id, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,6 +303,8 @@ export default class Room {
|
|||
}
|
||||
|
||||
buildModBadgeCSS() {
|
||||
if ( this.destroyed )
|
||||
return;
|
||||
if ( ! this.data || ! this.data.mod_urls || ! this.manager.context.get('chat.badges.custom-mod') )
|
||||
return this.style.delete('mod-badge');
|
||||
|
||||
|
@ -321,6 +329,8 @@ export default class Room {
|
|||
}
|
||||
|
||||
buildBadgeCSS() {
|
||||
if ( this.destroyed )
|
||||
return;
|
||||
if ( ! this.badges )
|
||||
return this.style.delete('badges');
|
||||
|
||||
|
@ -367,6 +377,8 @@ export default class Room {
|
|||
}
|
||||
|
||||
buildBitsCSS() {
|
||||
if ( this.destroyed )
|
||||
return;
|
||||
if ( ! this.bitsConfig )
|
||||
return this.style.delete('bits');
|
||||
|
||||
|
|
|
@ -477,8 +477,9 @@ export const AddonEmotes = {
|
|||
},
|
||||
|
||||
tooltip(target, tip) {
|
||||
const provider = target.dataset.provider,
|
||||
modifiers = target.dataset.modifierInfo;
|
||||
const ds = target.dataset,
|
||||
provider = ds.provider,
|
||||
modifiers = ds.modifierInfo;
|
||||
|
||||
let preview, source, owner, mods;
|
||||
|
||||
|
@ -496,7 +497,7 @@ export const AddonEmotes = {
|
|||
}
|
||||
|
||||
if ( provider === 'twitch' ) {
|
||||
const emote_id = parseInt(target.dataset.id, 10),
|
||||
const emote_id = parseInt(ds.id, 10),
|
||||
set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
|
||||
emote_set = set_id != null && this.emotes.getTwitchSetChannel(set_id, tip.rerender);
|
||||
|
||||
|
@ -519,8 +520,8 @@ export const AddonEmotes = {
|
|||
}
|
||||
|
||||
} else if ( provider === 'ffz' ) {
|
||||
const emote_set = this.emotes.emote_sets[target.dataset.set],
|
||||
emote = emote_set && emote_set.emotes[target.dataset.id];
|
||||
const emote_set = this.emotes.emote_sets[ds.set],
|
||||
emote = emote_set && emote_set.emotes[ds.id];
|
||||
|
||||
if ( emote_set )
|
||||
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
|
||||
|
@ -538,6 +539,9 @@ export const AddonEmotes = {
|
|||
}
|
||||
}
|
||||
|
||||
const name = ds.name || target.alt,
|
||||
hide_source = ds.noSource === 'true';
|
||||
|
||||
return [
|
||||
preview && this.context.get('tooltip.emote-images') && (<img
|
||||
class="preview-image"
|
||||
|
@ -545,9 +549,9 @@ export const AddonEmotes = {
|
|||
onLoad={tip.update}
|
||||
/>),
|
||||
|
||||
this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: target.alt}),
|
||||
(hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: %{code}', {code: ds.name || target.alt}),
|
||||
|
||||
source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
||||
! hide_source && source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
||||
{source}
|
||||
</div>),
|
||||
|
||||
|
@ -555,6 +559,8 @@ export const AddonEmotes = {
|
|||
{owner}
|
||||
</div>),
|
||||
|
||||
ds.sellout && (<div class="tw-mg-t-05 tw-border-t tw-pd-t-05">{ds.sellout}</div>),
|
||||
|
||||
mods && (<div class="tw-pd-t-1">{mods}</div>)
|
||||
];
|
||||
},
|
||||
|
|
|
@ -112,6 +112,7 @@ export default class User {
|
|||
if ( ! this.emote_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.emote_sets.push(provider, set_id);
|
||||
this.manager.emotes.refSet(set_id);
|
||||
this.manager.emotes.emit(':update-user-sets', this, provider, set_id, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +121,7 @@ export default class User {
|
|||
if ( this.emote_sets.sourceIncludes(provider, set_id) ) {
|
||||
this.emote_sets.remove(provider, set_id);
|
||||
this.manager.emotes.unrefSet(set_id);
|
||||
this.manager.emotes.emit(':update-user-sets', this, provider, set_id, false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,15 +30,13 @@
|
|||
<li>Settings from the old version are not being imported.</li>
|
||||
<li>Settings cannot be searched.</li>
|
||||
<li>Emoji aren't displayed.</li>
|
||||
<li>The emote menu isn't finished.</li>
|
||||
<li>Tab-completion and advanced input isn't available.</li>
|
||||
<li>Advanced input isn't available.</li>
|
||||
</ul>
|
||||
|
||||
<p>And the biggest features still under development:</p>
|
||||
|
||||
<ul class="tw-mg-b-2">
|
||||
<li>Emoji Rendering</li>
|
||||
<li>Emotes Menu</li>
|
||||
<li>Chat Filtering (Highlighted Words, etc.)</li>
|
||||
<li>Room Status Indicators</li>
|
||||
<li>Custom Mod Cards</li>
|
||||
|
@ -49,7 +47,6 @@
|
|||
<li>Portrait Mode</li>
|
||||
<li>Importing and exporting settings</li>
|
||||
<li>User Aliases</li>
|
||||
<li>Rich Content in Chat (aka Clip Embeds)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
|
|
997
src/sites/twitch-twilight/modules/chat/emote_menu.jsx
Normal file
997
src/sites/twitch-twilight/modules/chat/emote_menu.jsx
Normal file
|
@ -0,0 +1,997 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Chat Emote Menu
|
||||
// ============================================================================
|
||||
|
||||
import {has, get, once, set_equals} from 'utilities/object';
|
||||
import {KNOWN_CODES, TWITCH_EMOTE_BASE} from 'utilities/constants';
|
||||
|
||||
import Twilight from 'site';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
import SUB_STATUS from './sub_status.gql';
|
||||
|
||||
function maybe_date(val) {
|
||||
if ( ! val )
|
||||
return val;
|
||||
|
||||
try {
|
||||
return new Date(val);
|
||||
} catch(err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sort_sets(a, b) {
|
||||
const a_sk = a.sort_key,
|
||||
b_sk = b.sort_key;
|
||||
|
||||
if ( a_sk < b_sk ) return -1;
|
||||
if ( b_sk < a_sk ) return 1;
|
||||
|
||||
const a_n = a.title.toLowerCase(),
|
||||
b_n = b.title.toLowerCase();
|
||||
|
||||
if ( a_n < b_n ) return -1;
|
||||
if ( b_n < a_n ) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
export default class EmoteMenu extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.inject('settings');
|
||||
this.inject('i18n');
|
||||
this.inject('chat');
|
||||
this.inject('chat.badges');
|
||||
this.inject('chat.emotes');
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.apollo');
|
||||
this.inject('site.web_munch');
|
||||
this.inject('site.css_tweaks');
|
||||
|
||||
this.SUB_STATUS = SUB_STATUS;
|
||||
|
||||
this.settings.add('chat.emote-menu.enabled', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Emote Menu >> General',
|
||||
title: 'Use the FrankerFaceZ Emote Menu.',
|
||||
description: 'The FFZ emote menu replaces the built-in Twitch emote menu and provides enhanced functionality.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
changed: () => this.EmoteMenu.forceUpdate()
|
||||
});
|
||||
|
||||
this.settings.add('chat.emote-menu.icon', {
|
||||
requires: ['chat.emote-menu.enabled'],
|
||||
default: false,
|
||||
process(ctx, val) {
|
||||
return ctx.get('chat.emote-menu.enabled') ? val : false
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Emote Menu >> General',
|
||||
title: 'Replace the emote menu icon with the FFZ icon for that classic feel.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
})
|
||||
|
||||
this.EmoteMenu = this.fine.define(
|
||||
'chat-emote-menu',
|
||||
n => n.subscriptionProductHasEmotes,
|
||||
Twilight.CHAT_ROUTES
|
||||
)
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.on('i18n:update', () => this.EmoteMenu.forceUpdate());
|
||||
this.on('chat.emotes:update-default-sets', this.maybeUpdate, this);
|
||||
this.on('chat.emotes:update-user-sets', this.maybeUpdate, this);
|
||||
this.on('chat.emotes:update-room-sets', this.maybeUpdate, this);
|
||||
|
||||
this.chat.context.on('changed:chat.emote-menu.icon', val =>
|
||||
this.css_tweaks.toggle('emote-menu', val));
|
||||
|
||||
this.css_tweaks.toggle('emote-menu', this.chat.context.get('chat.emote-menu.icon'));
|
||||
|
||||
const t = this,
|
||||
React = this.web_munch.getModule('react'),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
if ( ! createElement )
|
||||
return t.log.warn('Unable to get React.');
|
||||
|
||||
this.defineClasses();
|
||||
|
||||
|
||||
this.EmoteMenu.ready(cls => {
|
||||
const old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
if ( ! this.props || ! has(this.props, 'channelOwnerID') || ! t.chat.context.get('chat.emote-menu.enabled') )
|
||||
return old_render.call(this);
|
||||
|
||||
return (<t.MenuComponent
|
||||
visible={this.props.visible}
|
||||
toggleVisibility={this.props.toggleVisibility}
|
||||
onClickEmote={this.props.onClickEmote}
|
||||
channel_data={this.props.channelData}
|
||||
emote_data={this.props.emoteSetsData}
|
||||
user_id={this.props.currentUserID}
|
||||
channel_id={this.props.channelOwnerID}
|
||||
loading={this.state.gqlLoading}
|
||||
error={this.state.gqlError}
|
||||
/>)
|
||||
}
|
||||
|
||||
this.EmoteMenu.forceUpdate();
|
||||
})
|
||||
}
|
||||
|
||||
maybeUpdate() {
|
||||
if ( this.chat.context.get('chat.emote-menu.enabled') )
|
||||
this.EmoteMenu.forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
defineClasses() {
|
||||
const t = this,
|
||||
storage = this.settings.provider,
|
||||
React = this.web_munch.getModule('react'),
|
||||
createElement = React && React.createElement;
|
||||
|
||||
this.MenuEmote = class FFZMenuEmote extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = props.onClickEmote.bind(this, props.data.name)
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
this.handleClick = this.props.onClickEmote.bind(this, this.props.data.name);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data,
|
||||
lock = this.props.lock,
|
||||
locked = this.props.locked,
|
||||
|
||||
sellout = lock ?
|
||||
this.props.all_locked ?
|
||||
t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) :
|
||||
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock)
|
||||
: null;
|
||||
|
||||
return (<button
|
||||
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
|
||||
data-tooltip-type="emote"
|
||||
data-provider={data.provider}
|
||||
data-id={data.id}
|
||||
data-set={data.set_id}
|
||||
data-no-source="true"
|
||||
data-name={data.name}
|
||||
aria-label={data.name}
|
||||
data-locked={data.locked}
|
||||
data-sellout={sellout}
|
||||
onClick={!data.locked && this.handleClick}
|
||||
>
|
||||
<figure class="emote-picker__emote-figure">
|
||||
<img
|
||||
class="emote-picker__emote-image"
|
||||
src={data.src}
|
||||
srcSet={data.srcSet}
|
||||
alt={data.name}
|
||||
/>
|
||||
</figure>
|
||||
{locked && (<figure class="ffz-i-lock" />)}
|
||||
</button>);
|
||||
}
|
||||
}
|
||||
|
||||
this.MenuSection = class FFZMenuSection extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const collapsed = storage.get('emote-menu.collapsed') || [];
|
||||
this.state = {collapsed: props.data && collapsed.includes(props.data.key)}
|
||||
|
||||
this.clickHeading = this.clickHeading.bind(this);
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
clickHeading() {
|
||||
if ( this.props.filter )
|
||||
return;
|
||||
|
||||
const collapsed = storage.get('emote-menu.collapsed') || [],
|
||||
key = this.props.data.key,
|
||||
idx = collapsed.indexOf(key),
|
||||
val = ! this.state.collapsed;
|
||||
|
||||
this.setState({collapsed: val});
|
||||
|
||||
if ( val && idx === -1 )
|
||||
collapsed.push(key);
|
||||
else if ( ! val && idx !== -1 )
|
||||
collapsed.splice(idx, 1);
|
||||
else
|
||||
return;
|
||||
|
||||
storage.set('emote-menu.collapsed', collapsed);
|
||||
}
|
||||
|
||||
onMouseEnter(event) {
|
||||
const set_id = parseInt(event.currentTarget.dataset.setId,10);
|
||||
this.setState({unlocked: set_id});
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
this.setState({unlocked: null});
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data,
|
||||
collapsed = ! this.props.filtered && this.state.collapsed;
|
||||
|
||||
if ( ! data )
|
||||
return null;
|
||||
|
||||
let image;
|
||||
if ( data.image )
|
||||
image = (<img src={data.image} />);
|
||||
else
|
||||
image = (<figure class={`ffz-i-${data.icon || 'zreknarf'}`} />);
|
||||
|
||||
let calendar;
|
||||
|
||||
const renews = data.renews && data.renews - new Date,
|
||||
ends = data.ends && data.ends - new Date;
|
||||
|
||||
if ( renews > 0 ) {
|
||||
const time = t.i18n.toHumanTime(renews / 1000);
|
||||
calendar = {
|
||||
icon: 'calendar',
|
||||
message: t.i18n.t('emote-menu.sub-renews', 'This sub renews in %{time}.', {time})
|
||||
}
|
||||
|
||||
} else if ( ends ) {
|
||||
const time = t.i18n.toHumanTime(ends / 1000);
|
||||
if ( data.prime )
|
||||
calendar = {
|
||||
icon: 'crown',
|
||||
message: t.i18n.t('emote-menu.sub-prime', 'This is your free sub with Twitch Prime.\nIt ends in %{time}.', {time})
|
||||
}
|
||||
else
|
||||
calendar = {
|
||||
icon: 'calendar-empty',
|
||||
message: t.i18n.t('emote-menu.sub-ends', 'This sub ends in %{time}.', {time})
|
||||
}
|
||||
}
|
||||
|
||||
return (<section data-key={data.key} class={this.props.filtered ? 'filtered' : ''}>
|
||||
<heading class="tw-pd-1 tw-border-b tw-flex" onClick={this.clickHeading}>
|
||||
{image}
|
||||
<div class="tw-pd-l-05">
|
||||
{data.title || t.i18n.t('emote-menu.unknown', 'Unknown Source')}
|
||||
{calendar && (<span
|
||||
class={`tw-mg-x-05 ffz--expiry-info ffz-tooltip ffz-i-${calendar.icon}`}
|
||||
data-tooltip-type="html"
|
||||
data-title={calendar.message}
|
||||
/>)}
|
||||
</div>
|
||||
<div class="tw-flex-grow-1" />
|
||||
{data.source || 'FrankerFaceZ'}
|
||||
<figure class={`tw-pd-l-05 ffz-i-${collapsed ? 'left' : 'down'}-dir`} />
|
||||
</heading>
|
||||
{collapsed || this.renderBody()}
|
||||
</section>)
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
const data = this.props.data,
|
||||
filtered = this.props.filtered,
|
||||
lock = data.locks && data.locks[this.state.unlocked],
|
||||
emotes = data.filtered_emotes && data.filtered_emotes.map(emote => (! filtered || ! emote.locked) && (<t.MenuEmote
|
||||
key={emote.id}
|
||||
onClickEmote={this.props.onClickEmote}
|
||||
data={emote}
|
||||
locked={emote.locked && (! lock || ! lock.emotes.has(emote.id))}
|
||||
all_locked={data.all_locked}
|
||||
lock={data.locks && data.locks[emote.set_id]}
|
||||
/>));
|
||||
|
||||
return (<div class="tw-pd-1 tw-border-b tw-c-background-alt tw-align-center">
|
||||
{emotes}
|
||||
{!filtered && this.renderSellout()}
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderSellout() {
|
||||
const data = this.props.data;
|
||||
|
||||
if ( ! data.all_locked || ! data.locks )
|
||||
return null;
|
||||
|
||||
const lock = data.locks[this.state.unlocked];
|
||||
|
||||
return (<div class="tw-mg-1 tw-border-t tw-pd-t-1 tw-mg-b-0">
|
||||
{lock ?
|
||||
t.i18n.t('emote-menu.sub-unlock', 'Subscribe for %{price} to unlock %{count} emote%{count|en_plural}', {price: lock.price, count: lock.emotes.size}) :
|
||||
t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')}
|
||||
<div class="ffz--sub-buttons tw-mg-t-05">
|
||||
{Object.values(data.locks).map(lock => (<a
|
||||
key={lock.price}
|
||||
class="tw-button"
|
||||
href={lock.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-set-id={lock.set_id}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<span class="tw-button__text">
|
||||
{lock.price}
|
||||
</span>
|
||||
</a>))}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
this.MenuComponent = class FFZEmoteMenuComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {page: null}
|
||||
this.componentWillReceiveProps(props);
|
||||
|
||||
this.clickTab = this.clickTab.bind(this);
|
||||
this.clickRefresh = this.clickRefresh.bind(this);
|
||||
this.handleFilterChange = this.handleFilterChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
|
||||
window.ffz_menu = this;
|
||||
}
|
||||
|
||||
clickTab(event) {
|
||||
this.setState({
|
||||
page: event.target.dataset.page
|
||||
});
|
||||
}
|
||||
|
||||
clickRefresh(event) {
|
||||
const target = event.currentTarget,
|
||||
tt = target && target._ffz_tooltip$0;
|
||||
|
||||
if ( tt && tt.hide )
|
||||
tt.hide();
|
||||
|
||||
this.setState({
|
||||
loading: true
|
||||
}, async () => {
|
||||
const props = this.props,
|
||||
promises = [],
|
||||
emote_data = props.emote_data,
|
||||
channel_data = props.channel_data;
|
||||
|
||||
if ( emote_data )
|
||||
promises.push(emote_data.refetch())
|
||||
|
||||
if ( channel_data )
|
||||
promises.push(channel_data.refetch());
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const es = props.emote_data && props.emote_data.emoteSets,
|
||||
sets = es && es.length ? new Set(es.map(x => parseInt(x.id, 10))) : new Set;
|
||||
|
||||
const data = await t.getData(sets, true);
|
||||
this.setState(this.filterState(this.state.filter, this.buildState(
|
||||
this.props,
|
||||
Object.assign({}, this.state, {set_sets: sets, set_data: data, loading: false})
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterChange(event) {
|
||||
this.setState(this.filterState(event.target.value, this.state));
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if ( event.keyCode === 27 )
|
||||
this.props.toggleVisibility();
|
||||
}
|
||||
|
||||
loadData(force = false, props, state) {
|
||||
state = state || this.state;
|
||||
if ( ! state )
|
||||
return false;
|
||||
|
||||
props = props || this.props;
|
||||
|
||||
const emote_sets = props.emote_data && props.emote_data.emoteSets,
|
||||
sets = Array.isArray(emote_sets) ? new Set(emote_sets.map(x => parseInt(x.id, 10))) : new Set;
|
||||
|
||||
force = force || (state.set_data && ! set_equals(state.set_sets, sets));
|
||||
|
||||
if ( state.set_data && ! force )
|
||||
return false;
|
||||
|
||||
this.setState({loading: true}, () => {
|
||||
t.getData(sets, force).then(d => {
|
||||
this.setState(this.filterState(this.state.filter, this.buildState(
|
||||
this.props,
|
||||
Object.assign({}, this.state, {set_sets: sets, set_data: d, loading: false})
|
||||
)));
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadSets(props) { // eslint-disable-line class-methods-use-this
|
||||
const emote_sets = props.emote_data && props.emote_data.emoteSets;
|
||||
if ( ! emote_sets || ! emote_sets.length )
|
||||
return;
|
||||
|
||||
for(const emote_set of emote_sets) {
|
||||
const set_id = parseInt(emote_set.id, 10);
|
||||
t.emotes.getTwitchSetChannel(set_id)
|
||||
}
|
||||
}
|
||||
|
||||
filterState(input, old_state) {
|
||||
const state = Object.assign({}, old_state);
|
||||
|
||||
state.filter = input;
|
||||
state.filtered = input && input.length > 0 && input !== ':' || false;
|
||||
|
||||
state.filtered_channel_sets = this.filterSets(input, state.channel_sets);
|
||||
state.filtered_all_sets = this.filterSets(input, state.all_sets);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
filterSets(input, sets) {
|
||||
const out = [];
|
||||
if ( ! sets || ! sets.length )
|
||||
return out;
|
||||
|
||||
const filtering = input && input.length > 0 && input !== ':';
|
||||
|
||||
for(const emote_set of sets) {
|
||||
const filtered = emote_set.filtered_emotes = emote_set.emotes.filter(emote =>
|
||||
! filtering || (! emote.locked && this.doesEmoteMatch(input, emote)));
|
||||
|
||||
if ( filtered.length )
|
||||
out.push(emote_set);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
doesEmoteMatch(filter, emote) { //eslint-disable-line class-methods-use-this
|
||||
if ( ! filter || ! filter.length )
|
||||
return true;
|
||||
|
||||
const emote_lower = emote.name.toLowerCase(),
|
||||
term_lower = filter.toLowerCase();
|
||||
|
||||
if ( ! filter.startsWith(':') )
|
||||
return emote_lower.includes(term_lower);
|
||||
|
||||
if ( emote_lower.startsWith(term_lower.slice(1)) )
|
||||
return true;
|
||||
|
||||
const idx = emote.name.indexOf(filter.charAt(1).toUpperCase());
|
||||
if ( idx !== -1 )
|
||||
return emote_lower.slice(idx+1).startsWith(term_lower.slice(2));
|
||||
}
|
||||
|
||||
|
||||
buildState(props, old_state) {
|
||||
const state = Object.assign({}, old_state),
|
||||
|
||||
data = state.set_data || {},
|
||||
channel = state.channel_sets = [],
|
||||
all = state.all_sets = [];
|
||||
|
||||
// If we're still loading, don't set any data.
|
||||
if ( props.loading || props.error || state.loading )
|
||||
return state;
|
||||
|
||||
// If we start loading because the sets we have
|
||||
// don't match, don't set any data either.
|
||||
if ( state.set_data && this.loadData(false, props, state) )
|
||||
return state;
|
||||
|
||||
// Start with the All tab. Some data calculated for
|
||||
// all is re-used for the Channel tab.
|
||||
|
||||
const emote_sets = props.emote_data && props.emote_data.emoteSets,
|
||||
inventory = t.emotes.twitch_inventory_sets || new Set,
|
||||
grouped_sets = {},
|
||||
set_ids = new Set;
|
||||
|
||||
if ( Array.isArray(emote_sets) )
|
||||
for(const emote_set of emote_sets) {
|
||||
if ( ! emote_set || ! Array.isArray(emote_set.emotes) )
|
||||
continue;
|
||||
|
||||
const set_id = parseInt(emote_set.id, 10),
|
||||
set_data = data[set_id] || {},
|
||||
more_data = t.emotes.getTwitchSetChannel(set_id),
|
||||
image = set_data.image;
|
||||
|
||||
set_ids.add(set_id);
|
||||
|
||||
let chan = set_data && set_data.user;
|
||||
if ( ! chan && more_data && more_data.c_id )
|
||||
chan = {
|
||||
id: more_data.c_id,
|
||||
login: more_data.c_name,
|
||||
display_name: more_data.c_name,
|
||||
bad: true
|
||||
};
|
||||
|
||||
let key = `twitch-set-${set_id}`,
|
||||
sort_key = 0,
|
||||
icon = 'twitch',
|
||||
title = chan && chan.display_name;
|
||||
|
||||
if ( title )
|
||||
key = `twitch-${chan.id}`;
|
||||
|
||||
else {
|
||||
if ( inventory.has(set_id) ) {
|
||||
title = t.i18n.t('emote-menu.inventory', 'Inventory');
|
||||
key = 'twitch-inventory';
|
||||
icon = 'inventory';
|
||||
sort_key = 50;
|
||||
|
||||
} else if ( more_data ) {
|
||||
title = more_data.c_name;
|
||||
|
||||
if ( title === '--global--' ) {
|
||||
title = t.i18n.t('emote-menu.global', 'Global Emotes');
|
||||
sort_key = 100;
|
||||
|
||||
} else if ( title === '--twitch-turbo--' || title === 'turbo' || title === '--turbo-faces--' ) {
|
||||
title = t.i18n.t('emote-menu.turbo', 'Turbo');
|
||||
sort_key = 75;
|
||||
|
||||
} else if ( title === '--prime--' || title === '--prime-faces--' ) {
|
||||
title = t.i18n.t('emote-menu.prime', 'Prime');
|
||||
icon = 'crown';
|
||||
sort_key = 75;
|
||||
}
|
||||
} else
|
||||
title = t.i18n.t('emote-menu.unknown-set', 'Set #%{set_id}', {set_id})
|
||||
}
|
||||
|
||||
let section, emotes;
|
||||
|
||||
if ( grouped_sets[key] ) {
|
||||
section = grouped_sets[key];
|
||||
emotes = section.emotes;
|
||||
|
||||
if ( chan && ! chan.bad && section.bad ) {
|
||||
section.title = title;
|
||||
section.image = image;
|
||||
section.icon = icon;
|
||||
section.sort_key = sort_key;
|
||||
section.bad = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
emotes = [];
|
||||
section = grouped_sets[key] = {
|
||||
sort_key,
|
||||
bad: chan ? chan.bad : true,
|
||||
key,
|
||||
image,
|
||||
icon,
|
||||
title,
|
||||
source: t.i18n.t('emote-menu.twitch', 'Twitch'),
|
||||
emotes,
|
||||
renews: set_data.renews,
|
||||
ends: set_data.ends,
|
||||
prime: set_data.prime
|
||||
}
|
||||
}
|
||||
|
||||
for(const emote of emote_set.emotes) {
|
||||
const id = parseInt(emote.id, 10),
|
||||
base = `${TWITCH_EMOTE_BASE}${id}`,
|
||||
name = KNOWN_CODES[emote.token] || emote.token;
|
||||
|
||||
emotes.push({
|
||||
provider: 'twitch',
|
||||
id,
|
||||
set_id,
|
||||
name,
|
||||
src: `${base}/1.0`,
|
||||
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`
|
||||
});
|
||||
}
|
||||
|
||||
if ( emotes.length && ! all.includes(section) )
|
||||
all.push(section);
|
||||
}
|
||||
|
||||
|
||||
// Now we handle the current Channel's emotes.
|
||||
|
||||
const user = props.channel_data && props.channel_data.user,
|
||||
products = user && user.subscriptionProducts;
|
||||
|
||||
if ( Array.isArray(products) ) {
|
||||
const badge = t.badges.getTwitchBadge('subscriber', '0', user.id, user.login),
|
||||
emotes = [],
|
||||
locks = {},
|
||||
section = {
|
||||
sort_key: -10,
|
||||
key: `twitch-${user.id}`,
|
||||
image: badge && badge.image1x,
|
||||
icon: 'twitch',
|
||||
title: t.i18n.t('emote-menu.sub-set', 'Subscriber Emotes'),
|
||||
source: t.i18n.t('emote-menu.twitch', 'Twitch'),
|
||||
emotes,
|
||||
locks,
|
||||
all_locked: true
|
||||
};
|
||||
|
||||
for(const product of products) {
|
||||
if ( ! product || ! Array.isArray(product.emotes) )
|
||||
continue;
|
||||
|
||||
const set_id = parseInt(product.emoteSetID, 10),
|
||||
set_data = data[set_id],
|
||||
locked = ! set_ids.has(set_id);
|
||||
|
||||
let lock_set;
|
||||
|
||||
if ( set_data ) {
|
||||
section.renews = set_data.renews;
|
||||
section.ends = set_data.ends;
|
||||
section.prime = set_data.prime;
|
||||
}
|
||||
|
||||
// If the channel is locked, store data about that in the
|
||||
// section so we can show appropriate UI to let people
|
||||
// subscribe. Also include all the already known emotes
|
||||
// in the list of emotes this product unlocks.
|
||||
if ( locked )
|
||||
locks[set_id] = {
|
||||
set_id,
|
||||
id: product.id,
|
||||
price: product.price,
|
||||
url: product.url,
|
||||
emotes: lock_set = new Set(emotes.map(e => e.id))
|
||||
}
|
||||
else
|
||||
section.all_locked = false;
|
||||
|
||||
for(const emote of product.emotes) {
|
||||
const id = parseInt(emote.id, 10),
|
||||
base = `${TWITCH_EMOTE_BASE}${id}`,
|
||||
name = KNOWN_CODES[emote.token] || emote.token;
|
||||
|
||||
emotes.push({
|
||||
provider: 'twitch',
|
||||
id,
|
||||
set_id,
|
||||
name,
|
||||
locked,
|
||||
src: `${base}/1.0`,
|
||||
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`
|
||||
});
|
||||
|
||||
if ( lock_set )
|
||||
lock_set.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if ( emotes.length )
|
||||
channel.push(section);
|
||||
}
|
||||
|
||||
|
||||
// Finally, emotes added by FrankerFaceZ.
|
||||
const me = t.site.getUser(),
|
||||
ffz_room = t.emotes.getRoomSetsWithSources(me.id, me.login, props.channel_id, null),
|
||||
ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login);
|
||||
|
||||
for(const [emote_set, provider] of ffz_room) {
|
||||
const section = this.processFFZSet(emote_set, provider);
|
||||
if ( section )
|
||||
channel.push(section);
|
||||
}
|
||||
|
||||
for(const [emote_set, provider] of ffz_global) {
|
||||
const section = this.processFFZSet(emote_set, provider);
|
||||
if ( section )
|
||||
all.push(section);
|
||||
}
|
||||
|
||||
|
||||
// Sort Sets
|
||||
channel.sort(sort_sets);
|
||||
all.sort(sort_sets);
|
||||
|
||||
state.has_channel_page = channel.length > 0;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
processFFZSet(emote_set, provider) { // eslint-disable-line class-methods-use-this
|
||||
if ( ! emote_set || ! emote_set.emotes )
|
||||
return null;
|
||||
|
||||
const pdata = t.emotes.providers.get(provider),
|
||||
source = pdata && pdata.name ?
|
||||
(pdata.i18n_key ?
|
||||
t.i18n.t(pdata.i18n_key, pdata.name, pdata) :
|
||||
pdata.name) :
|
||||
emote_set.source || 'FrankerFaceZ',
|
||||
|
||||
title = provider === 'main' ?
|
||||
t.i18n.t('emote-menu.main-set', 'Channel Emotes') :
|
||||
(emote_set.title || t.i18n.t('emote-menu.unknown', `Set #${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;
|
||||
|
||||
const emotes = [],
|
||||
section = {
|
||||
sort_key,
|
||||
key: `ffz-${emote_set.id}`,
|
||||
image: emote_set.icon,
|
||||
icon: 'zreknarf',
|
||||
title,
|
||||
source,
|
||||
emotes,
|
||||
};
|
||||
|
||||
for(const emote of Object.values(emote_set.emotes))
|
||||
if ( ! emote.hidden ) {
|
||||
const em = {
|
||||
provider: 'ffz',
|
||||
id: emote.id,
|
||||
set_id: emote_set.id,
|
||||
src: emote.urls[1],
|
||||
srcSet: emote.srcSet,
|
||||
name: emote.name
|
||||
};
|
||||
|
||||
emotes.push(em);
|
||||
}
|
||||
|
||||
if ( emotes.length )
|
||||
return section;
|
||||
}
|
||||
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if ( props.visible )
|
||||
this.loadData();
|
||||
|
||||
this.loadSets(props);
|
||||
|
||||
const state = this.buildState(props, this.state);
|
||||
this.setState(this.filterState(state.filter, state));
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (<div class="tw-align-center tw-pd-1">
|
||||
<div class="tw-mg-b-1">
|
||||
<div class="tw-mg-5">
|
||||
<img src="//cdn.frankerfacez.com/emoticon/26608/2" />
|
||||
</div>
|
||||
{t.i18n.t('emote-menu.error', 'There was an error rendering this menu.')}
|
||||
</div>
|
||||
<button class="tw-button" onClick={this.forceUpdate}>
|
||||
<span class="tw-button__text">
|
||||
{t.i18n.t('error.try-again', 'Try Again')}
|
||||
</span>
|
||||
</button>
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderEmpty() { // eslint-disable-line class-methods-use-this
|
||||
return (<div class="tw-align-center tw-pd-1">
|
||||
<div class="tw-mg-5">
|
||||
<img src="//cdn.frankerfacez.com/emoticon/26608/2" />
|
||||
</div>
|
||||
{this.state.filtered ?
|
||||
t.i18n.t('emote-menu.empty-search', 'There are no matching emotes.') :
|
||||
t.i18n.t('emote-menu.empty', "There's nothing here.")}
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderLoading() { // eslint-disable-line class-methods-use-this
|
||||
return (<div class="tw-align-center tw-pd-1">
|
||||
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
|
||||
{t.i18n.t('emote-menu.loading', 'Loading...')}
|
||||
</div>)
|
||||
}
|
||||
|
||||
render() {
|
||||
if ( ! this.props.visible )
|
||||
return null;
|
||||
|
||||
const loading = this.state.loading || this.props.loading;
|
||||
|
||||
let page = this.state.page, sets;
|
||||
if ( ! page )
|
||||
page = this.state.has_channel_page ? 'channel' : 'all';
|
||||
|
||||
switch(page) {
|
||||
case 'channel':
|
||||
sets = this.state.filtered_channel_sets;
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
sets = this.state.filtered_all_sets;
|
||||
break;
|
||||
}
|
||||
|
||||
return (<div
|
||||
class="tw-balloon tw-balloon--md tw-balloon--up tw-balloon--right tw-block tw-absolute ffz--emote-picker"
|
||||
data-a-target="emote-picker"
|
||||
>
|
||||
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background">
|
||||
<div
|
||||
class="emote-picker__tab-content scrollable-area"
|
||||
data-test-selector="scrollable-area-wrapper"
|
||||
data-simplebar
|
||||
>
|
||||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
{loading && this.renderLoading()}
|
||||
{!loading && sets && sets.map(data => (<t.MenuSection
|
||||
key={data.key}
|
||||
data={data}
|
||||
filtered={this.state.filtered}
|
||||
onClickEmote={this.props.onClickEmote}
|
||||
/>))}
|
||||
{! loading && (! sets || ! sets.length) && this.renderEmpty()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="emote-picker__controls-container tw-relative">
|
||||
<div class="tw-border-t tw-pd-1">
|
||||
<div class="tw-relative">
|
||||
<input
|
||||
type="text"
|
||||
class="tw-input"
|
||||
onChange={this.handleFilterChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder={t.i18n.t('emote-menu.search', 'Search for Emotes')}
|
||||
value={this.state.filter}
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="emote-picker__tabs-container tw-flex tw-border-t tw-c-background">
|
||||
{null && (<div class="ffz-tooltip emote-picker__tab tw-pd-x-1" data-tooltip-type="html" data-title="Favorites">
|
||||
<figure class="ffz-i-star" />
|
||||
</div>)}
|
||||
{this.state.has_channel_page && <div
|
||||
class={`emote-picker__tab tw-pd-x-1${page === 'channel' ? ' emote-picker__tab--active' : ''}`}
|
||||
id="emote-picker__channel"
|
||||
data-page="channel"
|
||||
onClick={this.clickTab}
|
||||
>
|
||||
{t.i18n.t('emote-menu.channel', 'Channel')}
|
||||
</div>}
|
||||
<div
|
||||
class={`emote-picker__tab tw-pd-x-1${page === 'all' ? ' emote-picker__tab--active' : ''}`}
|
||||
id="emote-picker__all"
|
||||
data-page="all"
|
||||
onClick={this.clickTab}
|
||||
>
|
||||
{t.i18n.t('emote-menu.all', 'All')}
|
||||
</div>
|
||||
<div class="tw-flex-grow-1" />
|
||||
{!loading && (<div
|
||||
class="ffz-tooltip emote-picker__tab tw-pd-x-1 tw-mg-r-0"
|
||||
data-tooltip-type="html"
|
||||
data-title="Refresh Data"
|
||||
onClick={this.clickRefresh}
|
||||
>
|
||||
<figure class="ffz-i-arrows-cw" />
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getData(sets, force) {
|
||||
if ( this._data ) {
|
||||
if ( ! force && set_equals(sets, this._data_sets) )
|
||||
return this._data;
|
||||
else {
|
||||
this._data = null;
|
||||
this._data_sets = null;
|
||||
}
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await this.apollo.client.query({
|
||||
query: SUB_STATUS,
|
||||
variables: {
|
||||
first: 100,
|
||||
criteria: {
|
||||
filter: 'ALL'
|
||||
}
|
||||
},
|
||||
fetchPolicy: force ? 'network-only' : 'cache-first'
|
||||
});
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('Error fetching additional emote menu data.', err);
|
||||
return this._data = null;
|
||||
}
|
||||
|
||||
const out = {},
|
||||
nodes = get('data.currentUser.subscriptionBenefits.edges.@each.node', data);
|
||||
|
||||
if ( nodes && nodes.length )
|
||||
for(const node of nodes) {
|
||||
const product = node.product,
|
||||
set_id = product && product.emoteSetID;
|
||||
|
||||
if ( ! set_id )
|
||||
continue;
|
||||
|
||||
const owner = product.owner || {},
|
||||
badges = owner.broadcastBadges;
|
||||
|
||||
let image;
|
||||
if ( badges )
|
||||
for(const badge of badges)
|
||||
if ( badge.setID === 'subscriber' && badge.version === '0' ) {
|
||||
image = badge.imageURL;
|
||||
break;
|
||||
}
|
||||
|
||||
out[set_id] = {
|
||||
ends: maybe_date(node.endsAt),
|
||||
renews: maybe_date(node.renewsAt),
|
||||
prime: node.purchasedWithPrime,
|
||||
|
||||
set_id: parseInt(set_id, 10),
|
||||
type: product.type,
|
||||
image,
|
||||
user: {
|
||||
id: owner.id,
|
||||
login: owner.login,
|
||||
display_name: owner.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._data_sets = sets;
|
||||
return this._data = out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
EmoteMenu.getData = once(EmoteMenu.getData);
|
|
@ -15,7 +15,7 @@ import Twilight from 'site';
|
|||
import Scroller from './scroller';
|
||||
import ChatLine from './line';
|
||||
import SettingsMenu from './settings_menu';
|
||||
//import EmoteMenu from './emote_menu';
|
||||
import EmoteMenu from './emote_menu';
|
||||
|
||||
|
||||
const CHAT_TYPES = (e => {
|
||||
|
@ -111,7 +111,7 @@ export default class ChatHook extends Module {
|
|||
this.inject(Scroller);
|
||||
this.inject(ChatLine);
|
||||
this.inject(SettingsMenu);
|
||||
//this.inject(EmoteMenu);
|
||||
this.inject(EmoteMenu);
|
||||
|
||||
|
||||
this.ChatController = this.fine.define(
|
||||
|
|
|
@ -144,7 +144,7 @@ export default class RichContent extends Module {
|
|||
return (<a
|
||||
class="chat-card__link"
|
||||
target="_blank"
|
||||
rel="noreferer noopener"
|
||||
rel="noreferrer noopener"
|
||||
href={this.state.url}
|
||||
>
|
||||
{this.renderCard()}
|
||||
|
|
37
src/sites/twitch-twilight/modules/chat/sub_status.gql
Normal file
37
src/sites/twitch-twilight/modules/chat/sub_status.gql
Normal file
|
@ -0,0 +1,37 @@
|
|||
query FFZ_EmoteMenu_SubStatus($first: Int, $after: Cursor, $criteria: SubscriptionBenefitCriteriaInput!) {
|
||||
currentUser {
|
||||
id
|
||||
subscriptionBenefits(first: $first, after: $after, criteria: $criteria) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
purchasedWithPrime
|
||||
endsAt
|
||||
renewsAt
|
||||
product {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
emoteSetID
|
||||
type
|
||||
owner {
|
||||
id
|
||||
login
|
||||
displayName
|
||||
broadcastBadges {
|
||||
id
|
||||
setID
|
||||
version
|
||||
imageURL(size: NORMAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,4 +33,79 @@
|
|||
.tw-ellipsis { line-height: 1.4rem }
|
||||
.chat-card__title { line-height: 1.5rem }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ffz--emote-picker {
|
||||
section:not(.filtered) heading {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: ffz-rotateplane 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ffz--expiry-info {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.emote-picker__tab {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.emote-picker__tab-content {
|
||||
max-height: 50rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 750px) {
|
||||
.emote-picker__tab-content {
|
||||
max-height: calc(100vh - 26rem);
|
||||
}
|
||||
}
|
||||
|
||||
.emote-picker__tab > *,
|
||||
.emote-picker__emote-link > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
section:last-of-type {
|
||||
& > div:last-child,
|
||||
& > heading:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.emote-picker__emote-link {
|
||||
position: relative;
|
||||
padding: 0.5rem;
|
||||
width: unset;
|
||||
height: unset;
|
||||
min-width: 3.8rem;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.locked {
|
||||
cursor: not-allowed;
|
||||
|
||||
img {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ffz-i-lock {
|
||||
position: absolute;
|
||||
bottom: 0; right: 0;
|
||||
border-radius: .2rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -330,7 +330,7 @@ export default class Fine extends Module {
|
|||
if ( idx !== -1 )
|
||||
this._waiting.splice(idx, 1);
|
||||
|
||||
this.log.info(`Found class for "${w.name}" at depth ${d.depth}`, d);
|
||||
this.log.info(`Found class for "${w.name}" at depth ${d.depth}`);
|
||||
w._set(d.cls, d.instances);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,33 @@ export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com
|
|||
export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl';
|
||||
export const API_SERVER = '//api.frankerfacez.com';
|
||||
|
||||
export const TWITCH_EMOTE_BASE = '//static-cdn.jtvnw.net/emoticons/v1/';
|
||||
|
||||
export const KNOWN_CODES = {
|
||||
'#-?[\\\\/]': '#-/',
|
||||
':-?(?:7|L)': ':-7',
|
||||
'\\<\\;\\]': '<]',
|
||||
'\\:-?(S|s)': ':-S',
|
||||
'\\:-?\\\\': ':-\\',
|
||||
'\\:\\>\\;': ':>',
|
||||
'B-?\\)': 'B-)',
|
||||
'\\:-?[z|Z|\\|]': ':-Z',
|
||||
'\\:-?\\)': ':-)',
|
||||
'\\:-?\\(': ':-(',
|
||||
'\\:-?(p|P)': ':-P',
|
||||
'\\;-?(p|P)': ';-P',
|
||||
'\\<\\;3': '<3',
|
||||
'\\:-?[\\\\/]': ':-/',
|
||||
'\\;-?\\)': ';-)',
|
||||
'R-?\\)': 'R-)',
|
||||
'[oO](_|\\.)[oO]': 'O.o',
|
||||
'[o|O](_|\\.)[o|O]': 'O.o',
|
||||
'\\:-?D': ':-D',
|
||||
'\\:-?(o|O)': ':-O',
|
||||
'\\>\\;\\(': '>(',
|
||||
'Gr(a|e)yFace': 'GrayFace'
|
||||
};
|
||||
|
||||
export const WS_CLUSTERS = {
|
||||
Production: [
|
||||
['wss://catbag.frankerfacez.com/', 0.25],
|
||||
|
|
|
@ -39,6 +39,39 @@ export function timeout(promise, delay) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure that a given asynchronous function is only called once
|
||||
* at a time.
|
||||
*/
|
||||
|
||||
export function once(fn) {
|
||||
let waiters;
|
||||
|
||||
return function(...args) {
|
||||
return new Promise(async (s,f) => {
|
||||
if ( waiters )
|
||||
return waiters.push([s,f]);
|
||||
|
||||
waiters = [[s,f]];
|
||||
let result;
|
||||
try {
|
||||
result = await fn.call(this, ...args); // eslint-disable-line no-invalid-this
|
||||
} catch(err) {
|
||||
for(const w of waiters)
|
||||
w[1](err);
|
||||
waiters = null;
|
||||
return;
|
||||
}
|
||||
|
||||
for(const w of waiters)
|
||||
w[0](result);
|
||||
|
||||
waiters = null;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that two arrays are the same length and that each array has the same
|
||||
* items in the same indices.
|
||||
|
@ -59,6 +92,18 @@ export function array_equals(a, b) {
|
|||
}
|
||||
|
||||
|
||||
export function set_equals(a,b) {
|
||||
if ( !(a instanceof Set) || !(b instanceof Set) || a.size !== b.size )
|
||||
return false;
|
||||
|
||||
for(const v of a)
|
||||
if ( ! b.has(v) )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Special logic to ensure that a target object is matched by a filter.
|
||||
* @param {object} filter The filter object
|
||||
|
|
|
@ -193,6 +193,8 @@ export class Tooltip {
|
|||
|
||||
// Set this early in case content uses it early.
|
||||
tip.update = () => tip._update(); // tip.popper && tip.popper.scheduleUpdate();
|
||||
tip.show = () => this.show(tip);
|
||||
tip.hide = () => this.hide(tip);
|
||||
tip.rerender = () => {
|
||||
if ( tip.visible ) {
|
||||
this.hide(tip);
|
||||
|
|
|
@ -109,11 +109,18 @@
|
|||
.ffz-i-pencil:before { content: '\e81a'; } /* '' */
|
||||
.ffz-i-info:before { content: '\e81b'; } /* '' */
|
||||
.ffz-i-help:before { content: '\e81c'; } /* '' */
|
||||
.ffz-i-calendar:before { content: '\e81d'; } /* '' */
|
||||
.ffz-i-left-dir:before { content: '\e81e'; } /* '' */
|
||||
.ffz-i-inventory:before { content: '\e81f'; } /* '' */
|
||||
.ffz-i-lock:before { content: '\e820'; } /* '' */
|
||||
.ffz-i-lock-open:before { content: '\e821'; } /* '' */
|
||||
.ffz-i-arrows-cw:before { content: '\e822'; } /* '' */
|
||||
.ffz-i-twitter:before { content: '\f099'; } /* '' */
|
||||
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
|
||||
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */
|
||||
.ffz-i-upload-cloud:before { content: '\f0ee'; } /* '' */
|
||||
.ffz-i-keyboard:before { content: '\f11c'; } /* '' */
|
||||
.ffz-i-calendar-empty:before { content: '\f133'; } /* '' */
|
||||
.ffz-i-ellipsis-vert:before { content: '\f142'; } /* '' */
|
||||
.ffz-i-twitch:before { content: '\f1e8'; } /* '' */
|
||||
.ffz-i-bell-off:before { content: '\f1f7'; } /* '' */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue