1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
* Fixed: Navigating between channels with the audio compressor enabled (or having previously been enabled) causing the player to become stuck loading infinitely. (Closes #1317)
* Fixed: Features on the `clips` and `player` subdomains not working correctly. (Closes #1336)
* Changed: Finish implementing the initial emote effects.
This commit is contained in:
SirStendec 2023-03-06 17:08:47 -05:00
parent e433aa3340
commit 915ad89f58
33 changed files with 839 additions and 152 deletions

2
.github/FUNDING.yml vendored
View file

@ -1 +1 @@
custom: ["https://www.frankerfacez.com/donate"]
custom: ["https://www.frankerfacez.com/subscribe"]

View file

@ -867,6 +867,20 @@
"css": "doc-text",
"code": 61686,
"src": "fontawesome"
},
{
"uid": "090b7864c67408ce29c67a49429b17a7",
"css": "fx",
"code": 59469,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M435.1 30C411.8 34 382.3 46.4 357.4 62.8 339.8 74.4 313.5 100.9 301.3 119.5 284.3 145.2 268.2 183 257.7 222.2L252.8 240.2 212.5 240.5 172.4 241 163.9 273.9C159.2 292.1 155.4 307.8 155.4 308.6 155.3 310.1 164.1 310.5 194.8 310.5 221 310.5 234.4 311 234.4 312 234.4 315.4 169.5 572.6 165.4 585.8 158.6 606.7 151.8 620.2 146.2 623.7 135.2 630.3 118.7 631.2 79.5 627.5 49.1 624.6 38.7 625.2 26.7 630.6-5.7 645.1-8.9 689.1 20.9 709.6 32.8 717.8 43.7 721 62.3 721.7 112.1 723.9 160.4 702.7 202.9 660.1 224.1 638.8 237 621.1 250.8 594 272 552.2 272.8 549.5 321.5 350.2L331.3 310.3 396.1 311.1C431.5 311.6 468.6 312.5 478.4 312.9L496.1 313.8 504.3 349.8C509 369.7 512.7 387 512.7 388.2 512.7 390.5 484.9 432.3 471.7 449.7 460.5 464.2 438.3 489.3 436.2 489.3 435.4 489.3 432.1 486.9 428.9 484 411.3 468.2 389.1 467.7 372.2 482.8 361.5 492.5 356.8 502.9 356.8 517.1 356.8 527.1 357.6 529.8 361.5 538 369.1 553.3 381.9 562.5 400.6 566.5 414.6 569.2 428.6 567.5 444.7 561 468.3 551.7 492.5 529.8 520.3 493.2 528.1 483 534.8 475 535.3 475.8 535.5 476.4 537.6 481.3 539.6 486.8 550.3 515.3 573.8 553.3 591.8 571.4 611 590.8 636.3 600.7 666.5 600.7 684.1 600.7 695.5 597.4 710.4 587.7 732.3 573.5 743.6 560.4 747.8 544.2 752.5 526 749.3 513.3 736.8 501 727.9 491.9 721.4 489.3 708.7 489.3 702.7 489.3 698.4 490.4 691.7 493.8 680.7 499.2 676.3 504.2 669.1 519.6 666.1 526.3 663.1 531.7 662.5 531.7 656.8 531.7 643.8 516.2 635.2 499.5 626.8 483.4 625.8 479.9 613.8 423.5L603.5 375.9 608.8 367.8C625.8 342.2 649.2 317.3 661.4 311.9 665.6 310 675.3 308.1 686.7 306.7 697 305.6 709.1 303.7 713.8 302.5 733.4 297.5 746.9 281.4 747.5 262.2 748.4 229.1 718.8 208.4 683.3 217.5 655.7 224.7 616.6 252.5 593.3 281.8 589.6 286.4 586.4 290 585.9 290 585.5 290 583.4 284.2 581.2 277.1 573.9 253.9 563.1 240.7 548.3 237.5 543.3 236.4 515 236.7 452.8 238.2 404 239.4 360.8 240.2 356.7 239.9L349.2 239.5 355.4 213.1C367.1 163.2 377.9 136.2 392.1 121.9 399 115 400.3 114.3 406.3 114.3 409.9 114.3 422 115.9 433.2 118.1 481.8 127 494.5 125.5 510.6 109 528.1 91 526.8 61.8 507.6 44.1 491.7 29.6 466.6 24.6 435.1 30Z",
"width": 1000
},
"search": [
"fx"
]
}
]
}

Binary file not shown.

View file

@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2022 by original authors @ fontello.com</metadata>
<metadata>Copyright (C) 2023 by original authors @ fontello.com</metadata>
<defs>
<font id="ffz-fontello" horiz-adv-x="1000" >
<font-face font-family="ffz-fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
@ -160,6 +160,8 @@
<glyph glyph-name="mange-suspicious" unicode="&#xe84c;" d="M450 300v-100h100v100h-100z m-200 353a500 500 0 0 1 226 81l24 16 24-16a500 500 0 0 1 278-84h48l-25-282a450 450 0 0 0-202-338l-123-80-123 80a450 450 0 0 0-202 338l-25 282h49c17 0 34 1 51 3z m9-100l15-176a350 350 0 0 1 158-263l68-45 68 45a350 350 0 0 1 158 263l15 176a600 600 0 0 0-191 52v-205h-100v205a600 600 0 0 0-191-52z" horiz-adv-x="1000" />
<glyph glyph-name="fx" unicode="&#xe84d;" d="M435 820c-23-4-53-16-78-33-17-11-43-38-56-56-17-26-33-64-43-103l-5-18-40 0-41-1-8-33c-5-18-9-34-9-35 0-1 9-1 40-1 26 0 39-1 39-2 0-3-64-261-69-274-6-21-13-34-19-38-11-6-27-7-66-3-31 2-41 2-53-4-33-14-36-58-6-79 12-8 23-11 41-12 50-2 98 19 141 62 21 21 34 39 48 66 21 42 22 45 71 244l9 40 65-1c36-1 73-1 82-2l18-1 8-36c5-20 9-37 9-38 0-2-28-44-41-62-11-14-34-39-36-39-1 0-4 2-7 5-18 16-40 16-57 1-10-9-15-20-15-34 0-10 1-13 5-21 7-15 20-24 39-28 14-3 28-1 44 5 23 9 48 31 75 68 8 10 15 18 15 17 1 0 3-5 5-11 10-28 34-66 52-84 19-20 44-30 75-30 17 0 29 4 43 13 22 15 34 28 38 44 5 18 1 31-11 43-9 9-16 12-28 12-6 0-11-1-17-5-11-5-16-10-23-26-3-6-6-12-6-12-6 0-19 16-28 33-8 16-9 19-21 76l-10 47 5 8c17 26 40 51 52 56 5 2 14 4 26 5 10 1 22 3 27 5 19 5 33 21 34 40 0 33-29 54-65 45-27-8-66-35-90-65-3-4-7-8-7-8 0 0-3 6-5 13-7 23-18 36-33 40-5 1-33 0-95-1-49-1-92-2-96-2l-8 1 6 26c12 50 23 77 37 91 7 7 8 8 14 8 4 0 16-2 27-4 49-9 62-7 78 9 17 18 16 47-3 65-16 14-41 19-73 14z" horiz-adv-x="1000" />
<glyph glyph-name="move" unicode="&#xf047;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -13,6 +13,7 @@ import SettingsManager from './settings/index';
import AddonManager from './addons';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import StagingSelector from './staging';
import Site from './sites/clips';
import Tooltips from 'src/modules/tooltips';
@ -52,6 +53,7 @@ class FrankerFaceZ extends Module {
this.inject('settings', SettingsManager);
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('staging', StagingSelector);
this.inject('site', Site);
this.inject('addons', AddonManager);

View file

@ -800,6 +800,10 @@ export class TranslationManager extends Module {
return this._.formatNumber(...args);
}
formatCurrency(...args) {
return this._.formatCurrency(...args);
}
formatDuration(...args) {
return this._.formatDuration(...args);
}

View file

@ -995,7 +995,7 @@ export default class Badges extends Module {
name = badge?.name;
let c = 0;
if ( name === 'supporter' || name === 'bot' ) {
if ( name === 'supporter' || name === 'subwoofer' || name === 'bot' ) {
this.setBulk('ffz-global', badge_id, data.users[badge_id].map(x => String(x)));
/*this.supporter_id = badge_id;
for(const user_id of data.users[badge_id])
@ -1032,8 +1032,8 @@ export default class Badges extends Module {
data.replaces = true;
}
if ( ! data.addon && (data.name === 'developer' || data.name === 'supporter') )
data.click_url = 'https://www.frankerfacez.com/donate';
if ( ! data.addon && (data.name === 'developer' || data.name === 'subwoofer' || data.name === 'supporter') )
data.click_url = 'https://www.frankerfacez.com/subscribe';
}
if ( generate_css )

View file

@ -11,6 +11,7 @@ import {NEW_API, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POINTS_SETS, TWI
import GET_EMOTE from './emote_info.gql';
import GET_EMOTE_SET from './emote_set_info.gql';
import { FFZEvent } from 'src/utilities/events';
const HoverRAF = Symbol('FFZ:Hover:RAF');
const HoverState = Symbol('FFZ:Hover:State');
@ -32,7 +33,9 @@ export const MODIFIER_FLAGS = make_enum_flags(
'Rainbow',
'HyperRed',
'Shake',
'Photocopy'
'Photocopy',
'Jam',
'Bounce'
);
export const MODIFIER_KEYS = Object.values(MODIFIER_FLAGS).filter(x => typeof x === 'number');
@ -48,19 +51,19 @@ const MODIFIER_FLAG_CSS = {
},
ShrinkX: {
title: 'Squish Horizontal',
transform: 'scaleX(0.5)'
//transform: 'scaleX(0.5)'
},
GrowX: {
title: 'Stretch Horizontal',
transform: 'scaleX(2)'
//transform: 'scaleX(2) translateX(25%)'
},
ShrinkY: {
title: 'Squish Vertical',
transform: 'scaleY(0.5)'
//transform: 'scaleY(0.5)'
},
GrowY: {
title: 'Stretch Vertical',
transform: 'scaleY(2)'
//transform: 'scaleY(2) translateY(25%)'
},
Rotate45: {
title: 'Rotate 45 Degrees',
@ -127,27 +130,89 @@ const MODIFIER_FLAG_CSS = {
}`
},
Photocopy: {
title: 'Photocopy',
filter: 'grayscale(1) brightness(0.65) contrast(5)'
title: 'Cursed',
filter: 'grayscale(1) brightness(0.7) contrast(2.5)'
},
Jam: {
title: 'Jam Animation',
animation: 'ffz-effect-jam 0.6s linear infinite',
animationTransform: 'ffz-effect-jam-transform 0.6s linear infinite',
raw: `@keyframes ffz-effect-jam {
0% { transform: translate(-2px, -2px) rotate(-6deg); }
10% { transform: translate(-1.5px, -2px) rotate(-8deg); }
20% { transform: translate(1px, -1.5px) rotate(-8deg); }
30% { transform: translate(3px, 2.5px) rotate(-6deg); }
40% { transform: translate(3px, 4px) rotate(-2deg); }
50% { transform: translate(2px, 4px) rotate(3deg); }
60% { transform: translate(1px, 4px) rotate(3deg); }
70% { transform: translate(-0.5px, 3px) rotate(2deg); }
80% { transform: translate(-1.25px, 1px) rotate(0deg); }
90% { transform: translate(-1.75px, -0.5px) rotate(-2deg); }
100% { transform: translate(-2px, -2px) rotate(-5deg); }
}
@keyframes ffz-effect-jam-transform {
0% { transform: var(--ffz-effect-transforms) translate(-2px, -2px) rotate(-6deg); }
10% { transform: var(--ffz-effect-transforms) translate(-1.5px, -2px) rotate(-8deg); }
20% { transform: var(--ffz-effect-transforms) translate(1px, -1.5px) rotate(-8deg); }
30% { transform: var(--ffz-effect-transforms) translate(3px, 2.5px) rotate(-6deg); }
40% { transform: var(--ffz-effect-transforms) translate(3px, 4px) rotate(-2deg); }
50% { transform: var(--ffz-effect-transforms) translate(2px, 4px) rotate(3deg); }
60% { transform: var(--ffz-effect-transforms) translate(1px, 4px) rotate(3deg); }
70% { transform: var(--ffz-effect-transforms) translate(-0.5px, 3px) rotate(2deg); }
80% { transform: var(--ffz-effect-transforms) translate(-1.25px, 1px) rotate(0deg); }
90% { transform: var(--ffz-effect-transforms) translate(-1.75px, -0.5px) rotate(-2deg); }
100% { transform: var(--ffz-effect-transforms) translate(-2px, -2px) rotate(-5deg); }
}`,
},
Bounce: {
animation: 'ffz-effect-bounce 0.5s linear infinite',
animationTransform: 'ffz-effect-bounce-transform 0.5s linear infinite',
transformOrigin: 'bottom center',
raw: `@keyframes ffz-effect-bounce {
0% { transform: scale(0.8, 1); }
10% { transform: scale(0.9, 0.8); }
20% { transform: scale(1, 0.4); }
25% { transform: scale(1.2, 0.3); }
25.001% { transform: scale(-1.2, 0.3); }
30% { transform: scale(-1, 0.4); }
40% { transform: scale(-0.9, 0.8); }
50% { transform: scale(-0.8, 1); }
60% { transform: scale(-0.9, 0.8); }
70% { transform: scale(-1, 0.4); }
75% { transform: scale(-1.2, 0.3); }
75.001% { transform: scale(1.2, 0.3); }
80% { transform: scale(1, 0.4); }
90% { transform: scale(0.9, 0.8); }
100% { transform: scale(0.8, 1); }
}
@keyframes ffz-effect-bounce-transform {
0% { transform: scale(0.8, 1) var(--ffz-effect-transforms); }
10% { transform: scale(0.9, 0.8) var(--ffz-effect-transforms); }
20% { transform: scale(1, 0.4) var(--ffz-effect-transforms); }
25% { transform: scale(1.2, 0.3) var(--ffz-effect-transforms); }
25.001% { transform: scale(-1.2, 0.3) var(--ffz-effect-transforms); }
30% { transform: scale(-1, 0.4) var(--ffz-effect-transforms); }
40% { transform: scale(-0.9, 0.8) var(--ffz-effect-transforms); }
50% { transform: scale(-0.8, 1) var(--ffz-effect-transforms); }
60% { transform: scale(-0.9, 0.8) var(--ffz-effect-transforms); }
70% { transform: scale(-1, 0.4) var(--ffz-effect-transforms); }
75% { transform: scale(-1.2, 0.3) var(--ffz-effect-transforms); }
75.001% { transform: scale(1.2, 0.3) var(--ffz-effect-transforms); }
80% { transform: scale(1, 0.4) var(--ffz-effect-transforms); }
90% { transform: scale(0.9, 0.8) var(--ffz-effect-transforms); }
100% { transform: scale(0.8, 1) var(--ffz-effect-transforms); }
}`
}
};
function generateBaseFilterCss() {
console.log('flags', MODIFIER_FLAGS);
console.log('css', MODIFIER_FLAG_CSS);
const out = [
`.modified-emote[data-effects] > img {
--ffz-effect-filters: none;
--ffz-effect-transforms: initial;
--ffz-effect-animations: initial;
}`/*,
`.modified-emote[data-effects] > img {
filter: var(--ffz-effect-filters);
transform: var(--ffz-effect-transforms);
animation: var(--ffz-effect-animations);
}`*/
}`
];
for(const [key, val] of Object.entries(MODIFIER_FLAG_CSS)) {
@ -223,6 +288,7 @@ export default class Emotes extends Module {
this.pending_effects = new Set();
this.applyEffects = this.applyEffects.bind(this);
this.sub_sets = new SourcedSet;
this.default_sets = new SourcedSet;
this.global_sets = new SourcedSet;
@ -436,7 +502,7 @@ export default class Emotes extends Module {
if ( ! this.parent.context.get('chat.effects.enable') )
return null;
let filter, transform, animation, animations = [];
let filter, transformOrigin, transform, animation, animations = [];
for(const key of MODIFIER_KEYS) {
if ( (flags & key) !== key || ! this.effects_enabled[key] )
@ -454,6 +520,9 @@ export default class Emotes extends Module {
? `${filter} ${input.filter}`
: input.filter;
if ( input.transformOrigin )
transformOrigin = input.transformOrigin;
if ( input.transform )
transform = transform
? `${transform} ${input.transform}`
@ -481,7 +550,8 @@ export default class Emotes extends Module {
return `.modified-emote[data-effects="${flags}"] > img {${filter ? `
--ffz-effect-filters: ${filter};
filter: var(--ffz-effect-filters);` : ''}${transform ? `
filter: var(--ffz-effect-filters);` : ''}${transformOrigin ? `
transform-origin: ${transformOrigin};` : ''}${transform ? `
--ffz-effect-transforms: ${transform};
transform: var(--ffz-effect-transforms);` : ''}${animation ? `
--ffz-effect-animations: ${animation};
@ -694,7 +764,6 @@ export default class Emotes extends Module {
this.settings.provider.set(key, favs);
}
handleClick(event) {
const target = event.target,
ds = target && target.dataset;
@ -710,7 +779,7 @@ export default class Emotes extends Module {
let url;
if ( provider === 'twitch' ) {
url = `https://twitchemotes.com/emotes/${ds.id}`;
url = null; // = `https://twitchemotes.com/emotes/${ds.id}`;
if ( click_sub ) {
const apollo = this.resolve('site.apollo');
@ -803,6 +872,16 @@ export default class Emotes extends Module {
return true;
}
const evt = new FFZEvent({
provider,
id: ds.id,
source: event
});
this.emit('chat.emotes:click', evt);
if ( evt.defaultPrevented )
return true;
if ( provider === 'twitch' && this.parent.context.get('chat.emote-dialogs') ) {
const fine = this.resolve('site.fine');
if ( ! fine )
@ -836,6 +915,55 @@ export default class Emotes extends Module {
// Access
// ========================================================================
getTargetEmote() {
const me = this.resolve('site').getUser(),
Input = me ? this.resolve('site.chat.input') : null,
entered = Input ? Input.getInput() : null;
const menu = this.resolve('site.chat.emote_menu')?.MenuWrapper?.first,
emote_sets = menu?.getAllSets?.(),
emotes = emote_sets
? emote_sets.map(x => x.emotes).flat().filter(x => ! x.effects)
: null;
if ( entered && emotes ) {
// Okay this is gonna be oof.
const name_map = {};
for(let i = 0; i < emotes.length; i++)
if ( ! name_map[emotes[i].name] )
name_map[emotes[i].name] = i;
const words = entered.split(' ');
let i = words.length;
while(i--) {
const word = words[i];
if ( name_map[word] != null )
return emotes[name_map[word]];
}
}
// Random emote
if ( emotes && emotes.length ) {
const idx = Math.floor(Math.random() * emotes.length),
emote = emotes[idx];
return emote;
}
// Return LaterSooner
return {
provider: 'ffz',
set_id: 3,
id: 149346,
name: 'LaterSooner',
src: 'https://cdn.frankerfacez.com/emote/149346/1',
srcSet: 'https://cdn.frankerfacez.com/emote/149346/1 1x, https://cdn.frankerfacez.com/emote/149346/2 2x, https://cdn.frankerfacez.com/emote/149346/4 4x',
width: 25,
height: 32
}
}
getSetIDs(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),
@ -949,6 +1077,21 @@ export default class Emotes extends Module {
.map(([set_id, source]) => [this.emote_sets[set_id], source]);
}
getSubSetIDsWithSources() {
const out = [], seen = new Set;
this._withSources(out, seen, this.sub_sets);
return out;
}
getSubSetsWithSources() {
return this.getSubSetIDsWithSources()
.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);
@ -1073,6 +1216,45 @@ export default class Emotes extends Module {
return false;
}
addSubSet(provider, set_id, data) {
if ( typeof set_id === 'number' )
set_id = `${set_id}`;
let changed = false, added = false;
if ( ! this.sub_sets.sourceIncludes(provider, set_id) ) {
changed = ! this.sub_sets.includes(set_id);
this.sub_sets.push(provider, set_id);
added = true;
}
if ( data )
this.loadSetData(set_id, data);
if ( changed ) {
this.refSet(set_id);
this.emit(':update-sub-sets', provider, set_id, true);
}
return added;
}
removeSubSet(provider, set_id) {
if ( typeof set_id === 'number' )
set_id = `${set_id}`;
if ( this.sub_sets.sourceIncludes(provider, set_id) ) {
this.sub_sets.remove(provider, set_id);
if ( ! this.sub_sets.includes(set_id) ) {
this.unrefSet(set_id);
this.emit(':update-sub-sets', provider, set_id, false);
}
return true;
}
return false;
}
refSet(set_id) {
this._set_refs[set_id] = (this._set_refs[set_id] || 0) + 1;
if ( this._set_timers[set_id] ) {
@ -1101,7 +1283,7 @@ export default class Emotes extends Module {
} catch(err) { /* do nothing */ }
try {
response = await fetch(`${this.staging.api}/v1/set/global${this.staging.active ? '/ids' : ''}`)
response = await fetch(`${this.staging.api}/v1/set/global/ids`)
} catch(err) {
tries++;
if ( tries < 10 )
@ -1127,8 +1309,12 @@ export default class Emotes extends Module {
this.addDefaultSet('ffz-global', set_id);
for(const set_id in sets)
if ( has(sets, set_id) )
if ( has(sets, set_id) ) {
const id = sets[set_id]?.id;
this.loadSetData(set_id, sets[set_id]);
if ( id && ! data.default_sets.includes(id) )
this.addSubSet('ffz-global', set_id);
}
if ( data.user_ids )
this.loadSetUserIds(data.user_ids);
@ -1262,6 +1448,7 @@ export default class Emotes extends Module {
text: emote.hidden ? '???' : emote.name,
length: emote.name.length,
height: emote.height,
width: emote.width,
source_modifier_flags: emote.modifier_flags ?? 0
};

View file

@ -10,6 +10,15 @@ import {has, getTwitchEmoteURL, split_chars, getTwitchEmoteSrcSet} from 'utiliti
import {EmoteTypes, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants';
import {CATEGORIES, JOINER_REPLACEMENT} from './emoji';
import { MODIFIER_FLAGS } from './emotes';
const SHRINK_X = MODIFIER_FLAGS.ShrinkX,
STRETCH_X = MODIFIER_FLAGS.GrowX,
SHRINK_Y = MODIFIER_FLAGS.ShrinkY,
STRETCH_Y = MODIFIER_FLAGS.GrowY,
ROTATE_45 = MODIFIER_FLAGS.Rotate45,
ROTATE_90 = MODIFIER_FLAGS.Rotate90;
const EMOTE_CLASS = 'chat-image chat-line__message--emote',
//WHITESPACE = /^\s*$/,
@ -1213,12 +1222,45 @@ export const AddonEmotes = {
hoverSrcSet = big ? token.animSrcSet2 : token.animSrcSet;
}
let style = undefined;
const effects = token.modifier_flags,
is_big = (token.big && ! token.can_big && token.height);
if ( effects ) {
this.emotes.ensureEffect(effects);
style = {
width: is_big ? token.width * 2 : token.width,
height: is_big ? token.height * 2 : token.height
}
if ( (effects & SHRINK_X) === SHRINK_X )
style.width *= 0.5;
if ( (effects & STRETCH_X) === STRETCH_X )
style.width *= 2;
if ( (effects & SHRINK_Y) === SHRINK_Y )
style.height *= 0.5;
if ( (effects & STRETCH_Y) === STRETCH_Y )
style.height *= 2;
if ( (effects & ROTATE_90) === ROTATE_90 ) {
const w = style.width;
style.width = style.height;
style.height = w;
}
if ( style.width > 128 )
style.width = 128;
if ( style.height > 40 )
style.height = 40;
}
const mods = token.modifiers || [], ml = mods.length,
emote = (<img
class={`${EMOTE_CLASS} ffz--pointer-events ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'twitch' ? ' twitch-emote' : token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`}
src={src}
srcSet={srcSet}
height={(token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined}
style={style}
height={style ? undefined : is_big ? `${token.height * 2}px` : undefined}
alt={token.text}
data-tooltip-type="emote"
data-provider={token.provider}
@ -1235,23 +1277,20 @@ export const AddonEmotes = {
onClick={this.emotes.handleClick}
/>);
if ( ! ml ) {
if ( ! ml && ! token.modifier_flags ) {
if ( wrapped )
return emote;
return (<div class="ffz--inline" data-test-selector="emote-button">{emote}</div>);
}
const effects = token.modifier_flags;
if ( effects )
this.emotes.ensureEffect(effects);
return (<div
class="ffz--inline ffz--pointer-events modified-emote"
class={`ffz--inline ffz--pointer-events modified-emote${style ? ' scaled-modified-emote' : ''}`}
data-test-selector="emote-button"
data-provider={token.provider}
data-id={token.id}
data-set={token.set}
style={style}
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
data-effects={effects ? effects : undefined}
onClick={this.emotes.handleClick}
@ -1369,6 +1408,76 @@ export const AddonEmotes = {
else if ( emote.urls[2] )
preview = emote.urls[2];
}
if ( ds.effects && emote.modifier && emote.modifier_flags ) {
owner = null;
const effects = emote.modifier_flags;
this.emotes.ensureEffect(effects);
const target = this.emotes.getTargetEmote();
let style = {
width: (target.width ?? 28) * 2,
height: (target.height ?? 28) * 2
};
let changed = false;
if ( (effects & SHRINK_X) === SHRINK_X ) {
style.width *= 0.5;
changed = true;
}
if ( (effects & STRETCH_X) === STRETCH_X ) {
style.width *= 2;
changed = true;
}
if ( (effects & SHRINK_Y) === SHRINK_Y ) {
style.height *= 0.5;
changed = true;
}
if ( (effects & STRETCH_Y) === STRETCH_Y ) {
style.height *= 2;
changed = true;
}
if ( changed ) {
if ( style.width > 512 )
style.width = 512;
if ( style.height > 160 )
style.height = 160;
}
style.width = `${style.width}px`;
style.height = `${style.height}px`;
// Whip up a special preview.
preview = (<div class="ffz-effect-tip">
<img
src={target.src}
srcSet={target.srcSet}
width={(target.width ?? 28) * 2}
height={(target.height ?? 28) * 2}
onLoad={tip.update}
/>
<span class="ffz-i-right-open"></span>
<div
class={`ffz--inline ffz--pointer-events modified-emote${style ? ' scaled-modified-emote' : ''}`}
style={style}
data-modifiers={emote.id}
data-effects={effects}
>
<img
class={`${EMOTE_CLASS} ffz--pointer-events ffz-tooltip ffz-emote`}
src={target.src}
srcSet={target.srcSet}
style={style}
height={style ? undefined : `${target.height * 2}px`}
onLoad={tip.update}
/>
</div>
</div>);
}
}
} else if ( provider === 'emoji' ) {
@ -1750,6 +1859,7 @@ export const TwitchEmotes = {
anim,
big,
can_big,
width: 28,
height: 28, // Not always accurate but close enough.
text: text.slice(e_start - t_start, e_end - t_start).join(''),
modifiers: [],

View file

@ -13,7 +13,7 @@ Due to performance problems with our current website, we have to use caching on
* *I don't want the `FFZ Supporter` badge.*
Users can toggle the visibility of their supporter badge at: [https://www.frankerfacez.com/donate](https://www.frankerfacez.com/donate)
Users can toggle the visibility of their FFZ badges at: [https://www.frankerfacez.com/settings/profile](https://www.frankerfacez.com/settings/profile)
* *I can see my emotes, but someone in chat said they can't.*

View file

@ -13,6 +13,7 @@ import SettingsManager from './settings/index';
import AddonManager from './addons';
import ExperimentManager from './experiments';
import {TranslationManager} from './i18n';
import StagingSelector from './staging';
import Site from './sites/player';
class FrankerFaceZ extends Module {
@ -49,6 +50,7 @@ class FrankerFaceZ extends Module {
this.inject('settings', SettingsManager);
this.inject('experiments', ExperimentManager);
this.inject('i18n', TranslationManager);
this.inject('staging', StagingSelector);
this.inject('site', Site);
this.inject('addons', AddonManager);

View file

@ -1443,6 +1443,47 @@ export default class PlayerBase extends Module {
tip.textContent = label;
}
replaceVideoElement(player, video) {
const new_vid = createElement('video'),
vol = video?._ffz_pregain_volume ?? video?.volume ?? player.getVolume(),
muted = player.isMuted();
new_vid._ffz_gain_value = video._ffz_gain_value;
new_vid._ffz_state = video._ffz_state;
new_vid._ffz_toggled = video._ffz_toggled;
new_vid._ffz_maybe_compress = video._ffz_compressed;
new_vid.volume = vol;
if ( muted )
new_vid.muted = true;
new_vid.playsInline = true;
this.installPlaybackRate(new_vid);
video.replaceWith(new_vid);
player.attachHTMLVideoElement(new_vid);
return new_vid;
}
hookPlayerLoad(player) {
if ( ! player || player._ffz_load )
return;
player._ffz_load = player.load;
player.load = (...args) => {
try {
const video = player.getHTMLVideoElement();
if ( video?._ffz_compressor && player.attachHTMLVideoElement ) {
this.log.info('Recreating video element due to player load with compressor installed.');
this.replaceVideoElement(player, video);
}
} catch(err) {
t.log.error('Error while handling player load.', err);
}
return player._ffz_load(...args);
}
}
compressPlayer(inst, e) {
const player = inst.props.mediaPlayerInstance,
core = player.core || player,
@ -1451,6 +1492,21 @@ export default class PlayerBase extends Module {
if ( ! video || ! HAS_COMPRESSOR )
return;
// Backup the player load method.
this.hookPlayerLoad(player);
// Backup and replace the setSrc method.
if ( ! inst._ffz_setSrc ) {
inst._ffz_setSrc = inst.setSrc;
inst.setSrc = async function(...args) {
console.log('setSrc', args);
const vid = inst.props.mediaPlayerInstance?.core?.mediaSinkManager?.video;
if ( vid && vid._ffz_compressor )
await this.resetPlayer(inst);
return inst._ffz_setSrc(...args);
}
}
// Backup the setVolume method.
if ( ! core._ffz_setVolume ) {
core._ffz_setVolume = core.setVolume;
@ -2028,7 +2084,7 @@ export default class PlayerBase extends Module {
}
resetPlayer(inst, e) {
async resetPlayer(inst, e) {
const player = inst ? ((inst.mediaSinkManager || inst.core?.mediaSinkManager) ? inst : inst?.props?.mediaPlayerInstance) : null;
if ( e ) {
@ -2064,40 +2120,20 @@ export default class PlayerBase extends Module {
const video = player.mediaSinkManager?.video || player.core?.mediaSinkManager?.video;
if ( video?._ffz_compressor && player.attachHTMLVideoElement ) {
const new_vid = createElement('video'),
vol = video?._ffz_pregain_volume ?? video?.volume ?? player.getVolume(),
muted = player.isMuted();
new_vid._ffz_gain_value = video._ffz_gain_value;
new_vid._ffz_state = video._ffz_state;
new_vid._ffz_toggled = video._ffz_toggled;
const new_vid = this.replaceVideoElement(player, video);
new_vid._ffz_maybe_compress = true;
new_vid.volume = muted ? 0 : vol;
new_vid.playsInline = true;
this.installPlaybackRate(new_vid);
video.replaceWith(new_vid);
player.attachHTMLVideoElement(new_vid);
setTimeout(() => {
player.setVolume(vol);
player.setMuted(muted);
//localStorage.volume = vol;
//localStorage.setItem('video-muted', JSON.stringify({default: muted}));
}, 0);
}
this.PlayerSource.check();
for(const inst of this.PlayerSource.instances) {
if ( ! player || player === inst.props?.mediaPlayerInstance )
inst.setSrc({isNewMediaPlayerInstance: false});
await inst.setSrc({isNewMediaPlayerInstance: false});
}
if ( position > 0 )
setTimeout(() => player.seekTo(position), 250);
}
addMetadata(inst) {
if ( ! this.metadata )
return;

View file

@ -150,6 +150,7 @@ export default class EmoteMenu extends Module {
constructor(...args) {
super(...args);
this.inject('staging');
this.inject('settings');
this.inject('i18n');
this.inject('chat');
@ -793,7 +794,7 @@ export default class EmoteMenu extends Module {
let source = data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source;
if ( source == null )
source = 'FrankerFaceZ';
source = 'FFZ';
return (<section ref={this.saveRef} data-key={data.key} class={filtered ? 'filtered' : ''} onMouseEnter={this.mouseEnter}>
{show_heading ? (<heading tabindex="0" class="tw-pd-1 tw-border-b tw-flex tw-flex-nowrap" onKeyDown={this.keyHeading} onClick={this.clickHeading}>
@ -833,7 +834,9 @@ export default class EmoteMenu extends Module {
let sellout = '';
if ( emote_lock ) {
if ( emote_lock.id === 'cheer' ) {
if ( emote_lock.id === 'subwoofer' ) {
sellout = t.i18n.t('emote-menu.emote-subwoofer', 'Become an FFZ Subwoofer to unlock this emote.');
} else if ( emote_lock.id === 'cheer' ) {
sellout = t.i18n.t('emote-menu.emote-cheer', 'Cheer an additional {bits_remaining, plural, one {# bit} other {# bits}} to unlock this emote.', emote_lock);
} else if ( emote_lock.id === 'follower' ) {
sellout = t.i18n.t('emote-menu.emote-follower', 'Follow {user} to unlock this emote in their channel.', emote_lock);
@ -886,6 +889,7 @@ export default class EmoteMenu extends Module {
data-set={emote.set_id}
data-code={emote.code}
data-modifiers={modifiers}
data-effects={emote.effects}
data-variant={emote.variant}
data-no-source={source}
data-name={emote.name}
@ -917,16 +921,27 @@ export default class EmoteMenu extends Module {
if ( ! data.all_locked || ! data.locks )
return null;
const lock = data.locks[this.state.unlocked],
locks = Object.values(data.locks).filter(x => x.id !== 'cheer');
let lock = data.locks[this.state.unlocked],
locks = Object.values(data.locks).filter(x => x.id !== 'cheer'),
has_ffz = locks.filter(x => x.is_ffz).length > 0;
if ( ! lock && data.locks.length === 1 )
lock = data.locks[0];
if ( ! locks.length )
return null;
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, plural, one {# emote} other {# emotes}}', {price: lock.price, count: lock.emotes.size}) :
t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')}
{has_ffz
? t.i18n.t('emote-menu.ffz-unlock', 'This feature is available to FFZ Subwoofers.')
: (lock
? t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count, plural, one {# emote} other {# emotes}}', {price: lock.price, count: lock.emotes.size})
: t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')
)
}
{has_ffz && this.props.ffz_sub_data?.has_free_sub
? <div class="tw-pd-y-1">{t.i18n.t('emote-menu.free-sub.about', 'As thanks for supporting us in the past, you can get one month of FFZ Subwoofer for free.')}</div>
: null}
<div class="ffz--sub-buttons tw-mg-t-05">
{locks.map(lock => lock.hide_button ? null : (<a
key={lock.price}
@ -939,7 +954,10 @@ export default class EmoteMenu extends Module {
onMouseLeave={this.onMouseLeave}
>
<span class="tw-button__text">
{lock.price}
{has_ffz && this.props.ffz_sub_data?.has_free_sub
? t.i18n.t('emote-menu.free-sub', 'Use My Free Month')
: lock.price
}
</span>
</a>))}
</div>
@ -1092,8 +1110,13 @@ export default class EmoteMenu extends Module {
tone: t.settings.provider.get('emoji-tone', null)
}
if ( props.visible )
if ( props.visible ) {
this.loadData();
if ( this.state.wants_plan_info )
this.loadFFZPlanData();
if ( this.state.wants_resub_info )
this.loadFFZSubData();
}
this.rebuildData();
@ -1310,6 +1333,9 @@ export default class EmoteMenu extends Module {
case 'channel':
sets = this.state.filtered_channel_sets;
break;
case 'effects':
sets = this.state.filtered_effect_sets;
break;
case 'emoji':
sets = this.state.filtered_emoji_sets;
break;
@ -1437,6 +1463,46 @@ export default class EmoteMenu extends Module {
return true;
}
loadFFZPlanData(force = false, props, state) {
state = state || this.state;
if ( ! state || state.ffz_plan_loading )
return false;
if ( state.ffz_sub_data && ! force )
return false;
this.setState({ffz_plan_loading: true}, () => {
t.getFFZSubPrices().then(d => {
this.setState(this.filterState(this.state.filter, this.buildState(
this.props,
Object.assign({}, this.state, {ffz_plan_data: d, ffz_plan_loading: false})
)));
})
});
return true;
}
loadFFZSubData(force = false, props, state) {
state = state || this.state;
if ( ! state || state.ffz_loading )
return false;
if ( state.ffz_sub_data && ! force )
return false;
this.setState({ffz_loading: true}, () => {
t.getFFZSubData().then(d => {
this.setState(this.filterState(this.state.filter, this.buildState(
this.props,
Object.assign({}, this.state, {ffz_sub_data: d, ffz_loading: false})
)));
})
});
return true;
}
filterState(input, old_state, visibility_control) {
const state = Object.assign({}, old_state);
@ -1449,6 +1515,7 @@ export default class EmoteMenu extends Module {
state.filtered = input && input.length > 0 && input !== ':' || false;
state.filtered_channel_sets = this.filterSets(input, state.channel_sets, visibility_control);
state.filtered_effect_sets = this.filterSets(input, state.effect_sets, visibility_control);
state.filtered_all_sets = this.filterSets(input, state.all_sets, visibility_control);
state.filtered_fav_sets = this.filterSets(input, state.fav_sets, visibility_control);
state.filtered_emoji_sets = this.filterSets(input, state.emoji_sets, visibility_control);
@ -1619,6 +1686,13 @@ export default class EmoteMenu extends Module {
return state;
}
getAllSets() {
return [
...(this.state.channel_sets || []),
...(this.state.effect_sets || []),
...(this.state.all_sets || [])
];
}
getSorter() { // eslint-disable-line class-methods-use-this
return EMOTE_SORTERS[t.chat.context.get('chat.emote-menu.sort-emotes')] || EMOTE_SORTERS[0] || (() => 0);
@ -1631,6 +1705,7 @@ export default class EmoteMenu extends Module {
modifiers = state.emote_modifiers = {},
channel = state.channel_sets = [],
all = state.all_sets = [],
effects = state.effect_sets = [],
favorites = state.favorites = [];
// If we're still loading, don't set any data.
@ -1987,8 +2062,8 @@ export default class EmoteMenu extends Module {
hide_button: true,
emotes: lock_set = new Set()
}
else
section.all_locked = false;
/*else
section.all_locked = false;*/
let order = 0;
for(const emote of local.emotes) {
@ -2178,56 +2253,106 @@ export default class EmoteMenu extends Module {
}
}
let wants_resub_info = false,
wants_plan_info = false;
// Finally, emotes added by FrankerFaceZ.
if ( t.chat.context.get('chat.emotes.enabled') > 1 ) {
const me = t.site.getUser();
if ( me ) {
const ffz_room = t.emotes.getRoomSetsWithSources(me.id, me.login, props.channel_id, null),
ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login),
seen_favorites = {};
let grouped_sets = {};
const ffz_room = t.emotes.getRoomSetsWithSources(me?.id, me?.login, props.channel_id, null),
ffz_subs = t.emotes.getSubSetsWithSources(),
ffz_global = t.emotes.getGlobalSetsWithSources(me?.id, me?.login),
seen_sets = new Set(),
seen_favorites = {};
for(const [emote_set, provider] of ffz_room) {
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets);
if ( section ) {
section.emotes.sort(sort_emotes);
let grouped_sets = {};
if ( ! channel.includes(section) )
channel.push(section);
}
for(const [emote_set, provider] 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);
if ( section ) {
section.emotes.sort(sort_emotes);
if ( ! channel.includes(section) )
channel.push(section);
}
}
grouped_sets = {};
grouped_sets = {};
for(const [emote_set, provider] of ffz_global) {
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets);
if ( section ) {
section.emotes.sort(sort_emotes);
const global_set_ids = ffz_global.map(x => x?.[0]?.id);
if ( ! all.includes(section) )
all.push(section);
for(const [emote_set, provider] of ffz_subs) {
if ( seen_sets.has(emote_set) )
continue;
seen_sets.add(emote_set);
if ( ! channel.includes(section) && maybe_call(section.force_global, this, emote_set, props.channel_data && props.channel_data.user, me) )
channel.push(section);
}
const locked = ! global_set_ids.includes(emote_set.id);
wants_resub_info = true;
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked, state);
if ( section ) {
section.emotes.sort(sort_emotes);
if ( ! effects.includes(section) && section.has_effects )
effects.push(section);
else if ( ! all.includes(section) )
all.push(section);
}
}
grouped_sets = {};
for(const [emote_set, provider] of ffz_global) {
if ( seen_sets.has(emote_set) )
continue;
seen_sets.add(emote_set);
const section = this.processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets);
if ( section ) {
section.emotes.sort(sort_emotes);
if ( ! effects.includes(section) && section.has_effects )
effects.push(section);
else if ( ! all.includes(section) )
all.push(section);
if ( ! channel.includes(section) && maybe_call(section.force_global, this, emote_set, props.channel_data && props.channel_data.user, me) )
channel.push(section);
}
}
}
// Load FFZ sub data.
state.wants_resub_info = wants_resub_info;
state.wants_plan_info = wants_plan_info;
if ( this.props.visible ) {
if ( wants_plan_info )
this.loadFFZPlanData();
if ( wants_resub_info )
this.loadFFZSubData();
}
// Sort Sets
channel.sort(sort_sets);
effects.sort(sort_sets);
all.sort(sort_sets);
state.has_channel_tab = channel.length > 0;
state.has_effect_Tab = effects.length > 0;
return this.buildEmoji(state);
}
processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets) { // eslint-disable-line class-methods-use-this
processFFZSet(emote_set, provider, favorites, seen_favorites, grouped_sets, locked = false, state) { // eslint-disable-line class-methods-use-this
if ( ! emote_set || ! emote_set.emotes )
return null;
@ -2242,7 +2367,7 @@ export default class EmoteMenu extends Module {
(pdata.i18n_key ?
t.i18n.t(pdata.i18n_key, pdata.name, pdata) :
pdata.name) :
emote_set.source || 'FrankerFaceZ',
emote_set.source || 'FFZ',
title = provider === 'main' ?
t.i18n.t('emote-menu.main-set', 'Channel Emotes') :
@ -2252,7 +2377,7 @@ export default class EmoteMenu extends Module {
if ( sort_key == null )
sort_key = emote_set.title.toLowerCase().includes('global') ? 100 : 0;
let section, emotes;
let section, emotes, locks;
if ( grouped_sets[key] ) {
section = grouped_sets[key];
@ -2277,10 +2402,31 @@ export default class EmoteMenu extends Module {
title,
source,
emotes,
force_global: emote_set.force_global
force_global: emote_set.force_global,
all_locked: true
}
}
// Try to get resub info.
const resub = (state || this.state)?.ffz_sub_data?.sets?.[emote_set.id];
if ( resub ) {
section.renews = resub.next_bill_date;
section.ends = resub.expires_at;
}
if ( locked ) {
section.locks = section.locks || {};
section.locks[emote_set.id] = {
set_id: emote_set.id,
id: 'subwoofer',
is_ffz: true,
price: 'More Info',
url: 'https://www.frankerfacez.com/subscribe',
emotes: locks = new Set()
}
} else
section.all_locked = false;
for(const emote of Object.values(emote_set.emotes))
if ( ! emote.hidden ) {
const is_fav = known_favs.includes(emote.id),
@ -2292,18 +2438,27 @@ export default class EmoteMenu extends Module {
srcSet: emote.srcSet,
animSrc: emote.animSrc,
animSrcSet: emote.animSrcSet,
effects: emote.modifier ? emote.modifier_flags : 0,
name: emote.name,
favorite: is_fav,
locked: locked,
hidden: known_hidden.includes(emote.id),
height: emote.height,
width: emote.width
};
emotes.push(em);
if ( is_fav && ! seen_favs.has(emote.id) ) {
if ( ! locked && is_fav && ! seen_favs.has(emote.id) ) {
favorites.push(em);
seen_favs.add(emote.id);
}
if ( locked )
locks.add(emote.id);
if ( emote.modifier && emote.modifier_flags )
section.has_effects = true;
}
if ( emotes.length )
@ -2320,7 +2475,11 @@ export default class EmoteMenu extends Module {
componentDidUpdate(old_props) {
if ( this.props.visible && ! old_props.visible ) {
this.loadData();
return;
if ( this.state.wants_plan_info )
this.loadFFZPlanData();
if ( this.state.wants_resub_info )
this.loadFFZSubData();
}
if ( ! this.props.visible && old_props.visible ) {
@ -2406,23 +2565,25 @@ export default class EmoteMenu extends Module {
if ( ! loading )
this.loadedOnce = true;
let tab, sets, is_emoji, is_favs;
let tab, sets, is_emoji, is_favs, is_effect;
if ( no_tabs ) {
sets = [
this.state.filtered_fav_sets,
this.state.filtered_channel_sets,
this.state.filtered_effect_sets,
this.state.filtered_all_sets,
this.state.filtered_emoji_sets
].flat();
} else {
tab = this.state.tab || t.chat.context.get('chat.emote-menu.default-tab');
if ( (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) )
if ( (tab === 'effect' && ! this.state.has_effect_Tab) || (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) )
tab = 'all';
is_emoji = tab === 'emoji';
is_favs = tab === 'fav';
is_effect = tab === 'effect';
switch(tab) {
case 'fav':
@ -2431,6 +2592,9 @@ export default class EmoteMenu extends Module {
case 'channel':
sets = this.state.filtered_channel_sets;
break;
case 'effect':
sets = this.state.filtered_effect_sets;
break;
case 'emoji':
sets = this.state.filtered_emoji_sets;
break;
@ -2467,6 +2631,7 @@ export default class EmoteMenu extends Module {
key: data.key,
idx,
data,
ffz_sub_data: this.state.ffz_sub_data,
emote_modifiers: this.state.emote_modifiers,
animated: this.state.animated,
combineTabs: this.state.combineTabs,
@ -2606,6 +2771,20 @@ export default class EmoteMenu extends Module {
</div>
</button>
</div>}
{this.state.has_effect_Tab && <div class={`emote-picker-tab-item${tab === 'effect' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'effect' ? ' ffz-interactable--selected' : ''}`}
id="emote-picker__effect"
data-tab="effect"
data-tooltip-type="html"
data-title={t.i18n.t('emote-menu.effects', 'Emote Effects')}
onClick={this.clickTab}
>
<div class="tw-inline-flex tw-pd-x-1 tw-pd-y-05 tw-font-size-4">
<figure class="ffz-i-fx" />
</div>
</button>
</div>}
<div class={`emote-picker-tab-item${tab === 'all' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button
class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'all' ? ' ffz-interactable--selected' : ''}`}
@ -2660,6 +2839,105 @@ export default class EmoteMenu extends Module {
}
async getFFZSubPrices() {
let result;
try {
result = await fetch(`${this.staging.api}/payment/plans`)
.then(r => r.ok ? r.json() : null);
} catch(err) {
this.log.error('Unable to load subscription prices from server.', err);
result = null;
}
// We only care about:
// 1. What collections are granted by the available plan.
// 2. How much they cost.
const out = {
sets: {}
};
for(const plan of Object.values(result.plans)) {
if ( ! Array.isArray(plan.temporary_collections) )
continue;
let prices;
for(const gw_plan of Object.values(result.gateway_plans)) {
if ( gw_plan.plan_id === plan.id && gw_plan.months === 1 ) {
prices = gw_plan.prices;
break;
}
}
if ( prices )
for(const set_id of plan.temporary_collections) {
out.sets[set_id] = {
plan_id: plan.id,
prices
}
}
}
return out;
}
async getFFZSubData() {
const me = this.resolve('site').getUser();
if ( ! me )
return null;
const token = await this.resolve('socket').getBareAPIToken();
if ( ! token )
return null;
let result;
try {
result = await fetch(`${this.staging.api}/v2/subscription/status?include=plan`, {
headers: {
Authorization: `Bearer ${token}`
}
})
.then(r => r.ok ? r.json() : null);
} catch(err) {
this.log.error('Unable to load subscription status from server.', err);
result = null;
}
// We only care about:
// 1. If the user has a free sub available
// 2. What collections can expire/renew
// 3. When they expire/renew
if ( ! result )
return null;
const out = {
has_free_sub: result.user?.bonus_month_eligible ?? false,
sets: {}
};
if ( result.user?.active_subs )
for(const entry of Object.values(result.user.active_subs)) {
const plan = result.plans?.[entry.id];
if ( Array.isArray(plan?.temporary_collections) ) {
for(const set_id of plan.temporary_collections)
out.sets[set_id] = {
plan_id: entry.id,
expires_at: entry.expires_at
? new Date(entry.expires_at)
: null,
next_bill_date: entry.next_bill_date
? new Date(entry.next_bill_date)
: null
};
}
}
return out;
}
async getData(sets, force, cursor = null, nodes = []) {
if ( this._data ) {
if ( ! force && set_equals(sets, this._data_sets) )

View file

@ -2177,41 +2177,6 @@ export default class ChatHook extends Module {
}
}
/*const old_chat = this.onChatMessageEvent;
this.onChatMessageEvent = function(e) {
/*if ( e && e.sentByCurrentUser ) {
try {
e.message.user.emotes = findEmotes(
e.message.body,
i.ffzGetEmotes()
);
} catch(err) {
t.log.capture(err, {extra: e});
}
}* /
return old_chat.call(i, e);
}
const old_action = this.onChatActionEvent;
this.onChatActionEvent = function(e) {
/*if ( e && e.sentByCurrentUser ) {
try {
e.message.user.emotes = findEmotes(
e.message.body,
i.ffzGetEmotes()
);
} catch(err) {
t.log.capture(err, {extra: e});
}
}* /
return old_action.call(i, e);
}*/
const old_announce = this.onAnnouncementEvent;
this.onAnnouncementEvent = function(e) {
//console.log('announcement', e);

View file

@ -1013,6 +1013,18 @@ export default class Input extends Module {
return results;*/
}
getInput() {
for(const inst of this.ChatInput.instances) {
if ( ! inst.autocompleteInputRef || ! inst.state )
continue;
if ( inst.state.value )
return inst.state.value;
}
return null;
}
pasteMessage(room, message) {
for(const inst of this.ChatInput.instances) {
if ( inst?.props?.channelLogin !== room )

View file

@ -348,7 +348,7 @@ export default class SettingsMenu extends Module {
'chat.ffz-badge.about',
'This badge appears globally for users with FrankerFaceZ. Please visit the {website} to change this badge.',
{
website: (<a href="https://www.frankerfacez.com/donate" class="ffz-link" rel="noopener noreferrer" target="_blank">
website: (<a href="https://www.frankerfacez.com/subscribe" class="ffz-link" rel="noopener noreferrer" target="_blank">
{this.i18n.t('chat.ffz-badge.site-link', 'FrankerFaceZ website')}
</a>)
}

View file

@ -1,3 +1,13 @@
.message > .modified-emote > .chat-line__message--emote {
vertical-align: bottom !important;
}
.message > .scaled-modified-emote {
vertical-align: baseline;
& > .chat-line__message--emote { vertical-align: unset !important; }
}
.message > div > .chat-line__message--emote {
vertical-align: baseline;
padding-top: 5px;

View file

@ -16,6 +16,17 @@
}
}
.scaled-modified-emote {
display: inline-block;
vertical-align: middle;
& > .chat-line__message--emote {
position: absolute;
top: 0;
left: 0;
}
}
.chat-author__display-name,
.chat-author__intl-login {
cursor: pointer;
@ -337,6 +348,11 @@
}
}
.emote-picker__tab-content {
height: unset !important;
max-height: 30.5rem;
}
&.ffz--emote-picker__tall {
.whispers-thread .emote-picker-and-button & .emote-picker__tab-content {
max-height: 30rem;

View file

@ -168,7 +168,7 @@ export default class SocketClient extends Module {
if ( ! user || ! user.id )
return fail(new Error('Unable to get current user or not logged in.'));
const es = new EventSource(`https://api-test.frankerfacez.com/auth/ext_verify/${user.id}`);
const es = new EventSource(`https://api.frankerfacez.com/auth/ext_verify/${user.id}`);
on(es, 'challenge', event => {
const conn = this.resolve('site.chat')?.ChatService?.first?.client?.connection;

View file

@ -43,4 +43,4 @@ export default class StagingSelector extends Module {
this.emit(':updated', this.api, this.cdn);
}
}
}

View file

@ -162,7 +162,7 @@ export const WS_CLUSTERS = {
],
Development: [
['wss://127.0.0.1:8003/', 1]
['ws://127.0.0.1:7999/', 1]
]
}

View file

@ -111,5 +111,6 @@ export default [
"mod",
"flag",
"mange-suspicious",
"doc-text"
"doc-text",
"fx"
];

View file

@ -68,6 +68,20 @@ export const DEFAULT_TYPES = {
return this.formatNumber(val, node.f);
},
currency(val, node) {
if ( typeof val !== 'number' ) {
let new_val = parseFloat(val);
if ( isNaN(new_val) || ! isFinite(new_val) )
new_val = parseInt(val, 10);
if ( isNaN(new_val) || ! isFinite(new_val) )
return val;
val = new_val;
}
return this.formatCurrency(val, node.f);
},
date(val, node) {
return this.formatDate(val, node.f);
},
@ -216,6 +230,7 @@ export default class TranslationCore {
this.cache = new Map;
this.numberFormats = new Map;
this.currencyFormats = new Map;
this.formats = Object.assign({}, DEFAULT_FORMATS);
if ( options.formats )
@ -237,6 +252,7 @@ export default class TranslationCore {
if ( val !== this._locale ) {
this._locale = val;
this.numberFormats.clear();
this.currencyFormats.clear();
}
}
@ -257,6 +273,20 @@ export default class TranslationCore {
}
}
formatCurrency(value, currency) {
let formatter = this.currencyFormats.get(currency);
if ( ! formatter ) {
formatter = new Intl.NumberFormat(navigator.languages, {
style: 'currency',
currency
});
this.currencyFormats.set(currency, formatter);
}
return formatter.format(value);
}
formatNumber(value, format) {
let formatter = this.numberFormats.get(format);
if ( ! formatter ) {

View file

@ -76,6 +76,7 @@
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-fx:before { content: '\e84d'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -76,6 +76,7 @@
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-fx { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }

View file

@ -87,6 +87,7 @@
.ffz-i-mod { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
.ffz-i-flag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
.ffz-i-mange-suspicious { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
.ffz-i-fx { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
.ffz-i-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf047;&nbsp;'); }
.ffz-i-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
.ffz-i-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf099;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.eot?30569253');
src: url('../font/ffz-fontello.eot?30569253#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?30569253') format('woff2'),
url('../font/ffz-fontello.woff?30569253') format('woff'),
url('../font/ffz-fontello.ttf?30569253') format('truetype'),
url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.eot?59946237');
src: url('../font/ffz-fontello.eot?59946237#iefix') format('embedded-opentype'),
url('../font/ffz-fontello.woff2?59946237') format('woff2'),
url('../font/ffz-fontello.woff?59946237') format('woff'),
url('../font/ffz-fontello.ttf?59946237') format('truetype'),
url('../font/ffz-fontello.svg?59946237#ffz-fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'ffz-fontello';
src: url('../font/ffz-fontello.svg?30569253#ffz-fontello') format('svg');
src: url('../font/ffz-fontello.svg?59946237#ffz-fontello') format('svg');
}
}
*/
@ -131,6 +131,7 @@
.ffz-i-mod:before { content: '\e84a'; } /* '' */
.ffz-i-flag:before { content: '\e84b'; } /* '' */
.ffz-i-mange-suspicious:before { content: '\e84c'; } /* '' */
.ffz-i-fx:before { content: '\e84d'; } /* '' */
.ffz-i-move:before { content: '\f047'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */

View file

@ -60,6 +60,11 @@
}
}
.ffz-i-fx:before {
margin-top: 0.2rem;
vertical-align: middle;
}
.ffz-i-zreknarf:before {
width: 1.3em;
margin: .5rem .05rem 0;

View file

@ -215,6 +215,14 @@ body {
padding: .1rem .2rem;
}
.ffz-effect-tip {
margin: 3px auto 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.ffz-badge-tip {
margin: .2rem .4rem;
}