mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-03 17:48:30 +00:00
4.19.6
* Added: Display bits emote rewards in the FFZ Emote Menu alongside subscription emotes. * Changed: 2FA reward emotes are now labelled as such in their tool-tips. * Changed: Update the style for the chat pause notice at the bottom of chat to match the latest Twitch style and language. * Fixed: Disabling hosting not working on Mod View. * Fixed: Emote sets not being sorted correctly in the FFZ Emote Menu when combining multiple sets (such as multiple subscription tiers) into one section. * API Changed: The `setting-color-box` component now allows you to open the color picker up and not only down.
This commit is contained in:
parent
99eee7396d
commit
30b9a15c0d
12 changed files with 191 additions and 63 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.19.5",
|
"version": "4.19.6",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
"name": "New API Stress Testing",
|
"name": "New API Stress Testing",
|
||||||
"description": "Send duplicate requests to the new API server for load testing.",
|
"description": "Send duplicate requests to the new API server for load testing.",
|
||||||
"groups": [
|
"groups": [
|
||||||
{"value": true, "weight": 75},
|
{"value": true, "weight": 50},
|
||||||
{"value": false, "weight": 25}
|
{"value": false, "weight": 50}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -963,6 +963,9 @@ function determineEmoteType(emote) {
|
||||||
if ( emote.setID == 300238151 )
|
if ( emote.setID == 300238151 )
|
||||||
return EmoteTypes.ChannelPoints;
|
return EmoteTypes.ChannelPoints;
|
||||||
|
|
||||||
|
if ( emote.setID == 300374282 )
|
||||||
|
return EmoteTypes.TwoFactor;
|
||||||
|
|
||||||
const id = parseInt(emote.setID, 10);
|
const id = parseInt(emote.setID, 10);
|
||||||
if ( ! isNaN(id) && isFinite(id) && id >= 5e8 )
|
if ( ! isNaN(id) && isFinite(id) && id >= 5e8 )
|
||||||
return EmoteTypes.BitsTier;
|
return EmoteTypes.BitsTier;
|
||||||
|
@ -983,6 +986,9 @@ function determineSetType(set) {
|
||||||
if ( TWITCH_PRIME_SETS.includes(id) )
|
if ( TWITCH_PRIME_SETS.includes(id) )
|
||||||
return EmoteTypes.Prime;
|
return EmoteTypes.Prime;
|
||||||
|
|
||||||
|
if ( id == 300374282 )
|
||||||
|
return EmoteTypes.TwoFactor;
|
||||||
|
|
||||||
const owner = set.owner;
|
const owner = set.owner;
|
||||||
if ( owner ) {
|
if ( owner ) {
|
||||||
if ( owner.id == 139075904 )
|
if ( owner.id == 139075904 )
|
||||||
|
|
|
@ -1059,6 +1059,9 @@ export const AddonEmotes = {
|
||||||
} else if ( type === EmoteTypes.Prime || type === EmoteTypes.Turbo )
|
} else if ( type === EmoteTypes.Prime || type === EmoteTypes.Turbo )
|
||||||
source = this.i18n.t('emote.prime', 'Twitch Prime');
|
source = this.i18n.t('emote.prime', 'Twitch Prime');
|
||||||
|
|
||||||
|
else if ( type === EmoteTypes.TwoFactor )
|
||||||
|
source = this.i18n.t('emote.2fa', 'Twitch 2FA Emote');
|
||||||
|
|
||||||
else if ( type === EmoteTypes.LimitedTime )
|
else if ( type === EmoteTypes.LimitedTime )
|
||||||
source = this.i18n.t('emote.limited', 'Limited-Time Only Emote');
|
source = this.i18n.t('emote.limited', 'Limited-Time Only Emote');
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
:id="item.full_key"
|
:id="item.full_key"
|
||||||
ref="control"
|
ref="control"
|
||||||
:alpha="alpha"
|
:alpha="alpha"
|
||||||
|
:open-up="openUp"
|
||||||
:nullable="true"
|
:nullable="true"
|
||||||
:value="color"
|
:value="color"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@ -73,6 +74,13 @@ export default {
|
||||||
return this.value;
|
return this.value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openUp() {
|
||||||
|
if ( this.item.openUp != null )
|
||||||
|
return this.item.openUp;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
alpha() {
|
alpha() {
|
||||||
if ( this.item.alpha != null )
|
if ( this.item.alpha != null )
|
||||||
return this.item.alpha;
|
return this.item.alpha;
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default class Channel extends Module {
|
||||||
this.ChannelPage = this.fine.define(
|
this.ChannelPage = this.fine.define(
|
||||||
'channel-page',
|
'channel-page',
|
||||||
n => (n.updateHost && n.updateChannel && n.state && has(n.state, 'hostedChannel')) || (n.getHostedChannelLogin && n.handleHostingChange) || (n.onChatHostingChange && n.state && has(n.state, 'hostMode')),
|
n => (n.updateHost && n.updateChannel && n.state && has(n.state, 'hostedChannel')) || (n.getHostedChannelLogin && n.handleHostingChange) || (n.onChatHostingChange && n.state && has(n.state, 'hostMode')),
|
||||||
['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
|
['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following', 'mod-view']
|
||||||
);
|
);
|
||||||
|
|
||||||
this.RaidController = this.fine.define(
|
this.RaidController = this.fine.define(
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
import {get} from 'utilities/object';
|
import {get} from 'utilities/object';
|
||||||
|
import {createElement} from 'utilities/dom';
|
||||||
|
|
||||||
//import CHANNEL_QUERY from './channel_header_query.gql';
|
//import CHANNEL_QUERY from './channel_header_query.gql';
|
||||||
|
|
||||||
|
@ -57,6 +58,12 @@ export default class ChannelBar extends Module {
|
||||||
n => n.getTitle && n.getGame && n.renderGame,
|
n => n.getTitle && n.getGame && n.renderGame,
|
||||||
['user']
|
['user']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.ModWidget = this.fine.define(
|
||||||
|
'mod-widget',
|
||||||
|
n => n.renderToolbar && n.getToolbarControls && n.childContext,
|
||||||
|
['mod-view']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
@ -77,6 +84,15 @@ export default class ChannelBar extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*this.ModWidget.on('mount', this.updateModWidget, this);
|
||||||
|
this.ModWidget.on('update', this.updateModWidget, this);
|
||||||
|
|
||||||
|
this.ModWidget.ready((cls, instances) => {
|
||||||
|
for(const inst of instances)
|
||||||
|
this.updateModWidget(inst);
|
||||||
|
});*/
|
||||||
|
|
||||||
|
|
||||||
//this.VideoBar.on('unmount', this.unmountVideoBar, this);
|
//this.VideoBar.on('unmount', this.unmountVideoBar, this);
|
||||||
this.VideoBar.on('mount', this.updateVideoBar, this);
|
this.VideoBar.on('mount', this.updateVideoBar, this);
|
||||||
this.VideoBar.on('update', this.updateVideoBar, this);
|
this.VideoBar.on('update', this.updateVideoBar, this);
|
||||||
|
@ -89,6 +105,30 @@ export default class ChannelBar extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*updateModWidget(inst) {
|
||||||
|
const container = this.fine.getChildNode(inst);
|
||||||
|
if ( ! container || ! container.querySelector('.video-player-hosting-ui__container') )
|
||||||
|
return;
|
||||||
|
|
||||||
|
const header = container.querySelector('.mod-view-panel-header');
|
||||||
|
if ( ! header )
|
||||||
|
return;
|
||||||
|
|
||||||
|
let cont = header.querySelector('.ffz--stat-container');
|
||||||
|
|
||||||
|
if ( ! cont ) {
|
||||||
|
cont = <div class="ffz--stat-container tw-pd-l-05"></div>;
|
||||||
|
const contcont = header.querySelector(':scope > div:first-child > div');
|
||||||
|
if ( ! contcont )
|
||||||
|
return;
|
||||||
|
|
||||||
|
contcont.appendChild(cont);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info('mod-widget', inst, cont);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
updateVideoBar(inst) {
|
updateVideoBar(inst) {
|
||||||
const container = this.fine.getChildNode(inst),
|
const container = this.fine.getChildNode(inst),
|
||||||
timestamp = container && container.querySelector('[data-test-selector="date"]');
|
timestamp = container && container.querySelector('[data-test-selector="date"]');
|
||||||
|
|
|
@ -637,11 +637,17 @@ export default class EmoteMenu extends Module {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const locked = emote.locked && (! lock || ! lock.emotes.has(emote.id)),
|
const locked = emote.locked && (! lock || ! lock.emotes.has(emote.id)),
|
||||||
emote_lock = locked && data.locks && data.locks[emote.set_id],
|
emote_lock = locked && data.locks && data.locks[emote.set_id];
|
||||||
sellout = emote_lock ? (data.all_locked ?
|
let sellout = '';
|
||||||
t.i18n.t('emote-menu.emote-sub', 'Subscribe for {price} to unlock this emote.', emote_lock) :
|
|
||||||
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to {price} to unlock this emote.', emote_lock)
|
if ( emote_lock ) {
|
||||||
) : '';
|
if ( emote_lock.id === 'cheer' ) {
|
||||||
|
sellout = t.i18n.t('emote-menu.emote-cheer', 'Cheer an additional {bits_remaining,number} bit{bits_remaining,en_plural} to unlock this emote.', emote_lock);
|
||||||
|
} else if ( data.all_locked )
|
||||||
|
sellout = t.i18n.t('emote-menu.emote-sub', 'Subscribe for {price} to unlock this emote.', emote_lock);
|
||||||
|
else
|
||||||
|
sellout = t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to {price} to unlock this emote.', emote_lock);
|
||||||
|
}
|
||||||
|
|
||||||
return this.renderEmote(
|
return this.renderEmote(
|
||||||
emote,
|
emote,
|
||||||
|
@ -698,14 +704,18 @@ export default class EmoteMenu extends Module {
|
||||||
if ( ! data.all_locked || ! data.locks )
|
if ( ! data.all_locked || ! data.locks )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const lock = data.locks[this.state.unlocked];
|
const lock = data.locks[this.state.unlocked],
|
||||||
|
locks = Object.values(data.locks).filter(x => x.id !== 'cheer');
|
||||||
|
|
||||||
|
if ( ! locks.length )
|
||||||
|
return null;
|
||||||
|
|
||||||
return (<div class="tw-mg-1 tw-border-t tw-pd-t-1 tw-mg-b-0">
|
return (<div class="tw-mg-1 tw-border-t tw-pd-t-1 tw-mg-b-0">
|
||||||
{lock ?
|
{lock ?
|
||||||
t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count,number} emote{count,en_plural}', {price: lock.price, count: lock.emotes.size}) :
|
t.i18n.t('emote-menu.sub-unlock', 'Subscribe for {price} to unlock {count,number} emote{count,en_plural}', {price: lock.price, count: lock.emotes.size}) :
|
||||||
t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')}
|
t.i18n.t('emote-menu.sub-basic', 'Subscribe to unlock some emotes')}
|
||||||
<div class="ffz--sub-buttons tw-mg-t-05">
|
<div class="ffz--sub-buttons tw-mg-t-05">
|
||||||
{Object.values(data.locks).map(lock => (<a
|
{locks.map(lock => (<a
|
||||||
key={lock.price}
|
key={lock.price}
|
||||||
class="tw-button tw-border-radius-none"
|
class="tw-button tw-border-radius-none"
|
||||||
href={lock.url}
|
href={lock.url}
|
||||||
|
@ -1282,9 +1292,15 @@ export default class EmoteMenu extends Module {
|
||||||
if ( a.locked && ! b.locked ) return 1;
|
if ( a.locked && ! b.locked ) return 1;
|
||||||
|
|
||||||
if ( sort_tiers || a.locked || b.locked ) {
|
if ( sort_tiers || a.locked || b.locked ) {
|
||||||
|
if ( COLLATOR ) {
|
||||||
|
const result = COLLATOR.compare(a.set_id, b.set_id);
|
||||||
|
if ( result != 0 )
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
if ( a.set_id < b.set_id ) return -1;
|
if ( a.set_id < b.set_id ) return -1;
|
||||||
if ( a.set_id > b.set_id ) return 1;
|
if ( a.set_id > b.set_id ) return 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sorter(a,b);
|
return sorter(a,b);
|
||||||
}
|
}
|
||||||
|
@ -1494,9 +1510,10 @@ export default class EmoteMenu extends Module {
|
||||||
// Now we handle the current Channel's emotes.
|
// Now we handle the current Channel's emotes.
|
||||||
|
|
||||||
const user = props.channel_data && props.channel_data.user,
|
const user = props.channel_data && props.channel_data.user,
|
||||||
products = user && user.subscriptionProducts;
|
products = user && user.subscriptionProducts,
|
||||||
|
bits = user?.cheer?.badgeTierEmotes;
|
||||||
|
|
||||||
if ( Array.isArray(products) ) {
|
if ( Array.isArray(products) || Array.isArray(bits) ) {
|
||||||
const badge = t.badges.getTwitchBadge('subscriber', '0', user.id, user.login),
|
const badge = t.badges.getTwitchBadge('subscriber', '0', user.id, user.login),
|
||||||
emotes = [],
|
emotes = [],
|
||||||
locks = {},
|
locks = {},
|
||||||
|
@ -1506,13 +1523,14 @@ export default class EmoteMenu extends Module {
|
||||||
image: badge && badge.image1x,
|
image: badge && badge.image1x,
|
||||||
image_set: badge && `${badge.image1x} 1x, ${badge.image2x} 2x, ${badge.image4x} 4x`,
|
image_set: badge && `${badge.image1x} 1x, ${badge.image2x} 2x, ${badge.image4x} 4x`,
|
||||||
icon: 'twitch',
|
icon: 'twitch',
|
||||||
title: t.i18n.t('emote-menu.sub-set', 'Subscriber Emotes'),
|
title: t.i18n.t('emote-menu.main-set', 'Channel Emotes'),
|
||||||
source: t.i18n.t('emote-menu.twitch', 'Twitch'),
|
source: t.i18n.t('emote-menu.twitch', 'Twitch'),
|
||||||
emotes,
|
emotes,
|
||||||
locks,
|
locks,
|
||||||
all_locked: true
|
all_locked: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if ( Array.isArray(products) ) {
|
||||||
for(const product of products) {
|
for(const product of products) {
|
||||||
if ( ! product || ! Array.isArray(product.emotes) )
|
if ( ! product || ! Array.isArray(product.emotes) )
|
||||||
continue;
|
continue;
|
||||||
|
@ -1579,6 +1597,53 @@ export default class EmoteMenu extends Module {
|
||||||
lock_set.add(id);
|
lock_set.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( Array.isArray(bits) ) {
|
||||||
|
for(const emote of bits) {
|
||||||
|
if ( ! emote || ! emote.id || ! emote.bitsBadgeTierSummary )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const id = emote.id,
|
||||||
|
set_id = emote.setID,
|
||||||
|
summary = emote.bitsBadgeTierSummary,
|
||||||
|
locked = ! twitch_seen.has(id) && ! summary.self?.isUnlocked;
|
||||||
|
|
||||||
|
// If the emote isn't unlocked, store data about that in the
|
||||||
|
// section so we can show appropriate UI to let people know
|
||||||
|
// that the emote isn't unlocked.
|
||||||
|
if ( locked )
|
||||||
|
locks[set_id] = {
|
||||||
|
set_id,
|
||||||
|
id: 'cheer',
|
||||||
|
price: null,
|
||||||
|
bits: summary.threshold,
|
||||||
|
bits_remaining: summary.self?.numberOfBitsUntilUnlock ?? summary.threshold,
|
||||||
|
emotes: new Set([emote.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = `${TWITCH_EMOTE_BASE}${id}`,
|
||||||
|
is_fav = twitch_favorites.includes(id);
|
||||||
|
|
||||||
|
const em = {
|
||||||
|
provider: 'twitch',
|
||||||
|
id,
|
||||||
|
set_id,
|
||||||
|
name: emote.token,
|
||||||
|
locked,
|
||||||
|
src: `${base}/1.0`,
|
||||||
|
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`,
|
||||||
|
favorite: is_fav
|
||||||
|
};
|
||||||
|
|
||||||
|
emotes.push(em);
|
||||||
|
|
||||||
|
if ( ! locked && is_fav )
|
||||||
|
favorites.push(em);
|
||||||
|
|
||||||
|
twitch_seen.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ( emotes.length ) {
|
if ( emotes.length ) {
|
||||||
emotes.sort(sort_emotes);
|
emotes.sort(sort_emotes);
|
||||||
|
|
|
@ -588,18 +588,23 @@ export default class Scroller extends Module {
|
||||||
f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') :
|
f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') :
|
||||||
t.i18n.t('key.mouse', 'Mouse Movement');
|
t.i18n.t('key.mouse', 'Mouse Movement');
|
||||||
|
|
||||||
msg = t.i18n.t('chat.paused', '(Chat Paused Due to {reason})', {reason});
|
msg = t.i18n.t('chat.paused', 'Chat Paused Due to {reason}', {reason});
|
||||||
cls = 'ffz--freeze-indicator';
|
cls = 'ffz--freeze-indicator';
|
||||||
|
|
||||||
} else if ( this.state.isAutoScrolling )
|
} else if ( this.state.isAutoScrolling )
|
||||||
return null;
|
return null;
|
||||||
else
|
else
|
||||||
msg = t.i18n.t('chat.messages-below', 'More messages below.');
|
msg = t.i18n.t('chat.messages-below', 'Chat Paused Due to Scroll');
|
||||||
|
|
||||||
return createElement('div', {
|
return createElement('div', {
|
||||||
className: `chat-list__list-footer tw-absolute tw-align-items-center tw-border-radius-medium tw-bottom-0 tw-flex tw-full-width tw-justify-content-center tw-pd-05 ${cls}`,
|
className: `chat-list__list-footer tw-absolute tw-align-items-center tw-border-radius-medium tw-bottom-0 tw-flex tw-justify-content-center tw-mg-b-1 tw-pd-x-2 tw-pd-y-05 ${cls}`,
|
||||||
onClick: this.ffzFastResume
|
onClick: this.ffzFastResume
|
||||||
}, createElement('div', null, msg));
|
}, createElement('div', null, msg));
|
||||||
|
|
||||||
|
/*return createElement('div', {
|
||||||
|
className: `chat-list__list-footer tw-absolute tw-align-items-center tw-border-radius-medium tw-bottom-0 tw-flex tw-full-width tw-justify-content-center tw-pd-05 ${cls}`,
|
||||||
|
onClick: this.ffzFastResume
|
||||||
|
}, createElement('div', null, msg));*/
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.prototype.smoothScrollBottom = function() {
|
cls.prototype.smoothScrollBottom = function() {
|
||||||
|
|
|
@ -494,7 +494,7 @@ export default class Fine extends Module {
|
||||||
if ( idx !== -1 )
|
if ( idx !== -1 )
|
||||||
this._waiting.splice(idx, 1);
|
this._waiting.splice(idx, 1);
|
||||||
|
|
||||||
this.log.info(`Found class for "${w.name}" at depth ${d.depth}`);
|
this.log.debug(`Found class for "${w.name}" at depth ${d.depth}`);
|
||||||
w._set(d.cls, d.instances);
|
w._set(d.cls, d.instances);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ export default class WebMunch extends Module {
|
||||||
|
|
||||||
webpackJsonpv3(chunk_ids, modules) {
|
webpackJsonpv3(chunk_ids, modules) {
|
||||||
const names = chunk_ids.map(x => this._module_names[x] || x).join(', ');
|
const names = chunk_ids.map(x => this._module_names[x] || x).join(', ');
|
||||||
this.log.info(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
|
this.log.debug(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
|
||||||
this.log.debug(`Modules: ${Object.keys(modules)}`);
|
this.log.debug(`Modules: ${Object.keys(modules)}`);
|
||||||
|
|
||||||
const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params
|
const res = this._original_loader.apply(window, arguments); // eslint-disable-line prefer-rest-params
|
||||||
|
@ -129,7 +129,7 @@ export default class WebMunch extends Module {
|
||||||
modules = data[1],
|
modules = data[1],
|
||||||
names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._module_names[x] || x).join(', ');
|
names = Array.isArray(chunk_ids) && chunk_ids.map(x => this._module_names[x] || x).join(', ');
|
||||||
|
|
||||||
this.log.info(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
|
this.log.debug(`Twitch Chunk Loaded: ${chunk_ids} (${names})`);
|
||||||
this.log.debug(`Modules: ${Object.keys(modules)}`);
|
this.log.debug(`Modules: ${Object.keys(modules)}`);
|
||||||
|
|
||||||
if ( modules )
|
if ( modules )
|
||||||
|
|
|
@ -109,5 +109,6 @@ export const EmoteTypes = make_enum(
|
||||||
'Unavailable',
|
'Unavailable',
|
||||||
'Subscription',
|
'Subscription',
|
||||||
'BitsTier',
|
'BitsTier',
|
||||||
'Global'
|
'Global',
|
||||||
|
'TwoFactor'
|
||||||
);
|
);
|
Loading…
Add table
Add a link
Reference in a new issue