diff --git a/package.json b/package.json
index 9a5103b5..e022f158 100755
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
- "version": "4.63.0",
+ "version": "4.64.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
diff --git a/src/modules/chat/components/chat-rich.vue b/src/modules/chat/components/chat-rich.vue
index a9cd75e0..db4b4f72 100644
--- a/src/modules/chat/components/chat-rich.vue
+++ b/src/modules/chat/components/chat-rich.vue
@@ -71,7 +71,7 @@ export default {
methods: {
handleClick(event) {
- if ( ! this.events.emit || event.ctrlKey || event.shiftKey )
+ if ( ! this.events?.emit || event.ctrlKey || event.shiftKey )
return;
const target = event.currentTarget,
@@ -392,4 +392,4 @@ export default {
}
}
-
\ No newline at end of file
+
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index 8a6d4ffb..3bb5d468 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -6,12 +6,12 @@
import dayjs from 'dayjs';
-import { DEBUG, LINK_DATA_HOSTS } from 'utilities/constants';
+import { DEBUG, LINK_DATA_HOSTS, RESOLVERS_REQUIRE_TOS } from 'utilities/constants';
import Module, { buildAddonProxy } from 'utilities/module';
import {Color} from 'utilities/color';
import {createElement, ManagedStyle} from 'utilities/dom';
import {getFontsList} from 'utilities/fonts';
-import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars, makeAddonIdChecker} from 'utilities/object';
+import {timeout, has, addWordSeparators, glob_to_regex, escape_regex, split_chars, makeAddonIdChecker, deep_copy} from 'utilities/object';
import Badges from './badges';
import Emotes from './emotes';
@@ -109,10 +109,10 @@ export default class Chat extends Module {
this.__link_providers = [];
this._hl_reasons = {};
- this.addHighlightReason('mention', 'Mentioned');
- this.addHighlightReason('user', 'Highlight User');
- this.addHighlightReason('badge', 'Highlight Badge');
- this.addHighlightReason('term', 'Highlight Term');
+ this.addHighlightReason('mention', 'Mentioned', 'Mention');
+ this.addHighlightReason('user', 'Highlight User', 'User');
+ this.addHighlightReason('badge', 'Highlight Badge', 'Badge');
+ this.addHighlightReason('term', 'Highlight Term', 'Term');
// ========================================================================
// Settings
@@ -1085,6 +1085,19 @@ export default class Chat extends Module {
});
+ // Terms of Service Stuff
+ for(const [key, info] of Object.entries(RESOLVERS_REQUIRE_TOS)) {
+ this.settings.addUI(`tooltip.tos.${key}`, {
+ path: 'Chat > Tooltips >> Terms of Service @{"description": "The following services require you to agree to their Terms of Service before we can show you information from their platforms."}',
+ component: 'tooltip-tos',
+ item: key,
+ override_setting: 'agreed-tos',
+ data: deep_copy(info),
+ onUIChange: () => this.emit(':update-link-resolver')
+ });
+ }
+
+
this.settings.add('chat.adjustment-mode', {
default: null,
process(ctx, val) {
@@ -1486,6 +1499,8 @@ export default class Chat extends Module {
this.socket = this.resolve('socket');
this.pubsub = this.resolve('pubsub');
+ this.settings.provider.on('changed', this.onProviderChange, this);
+
this.on('site.subpump:pubsub-message', this.onPubSub, this);
if ( this.context.get('chat.filtering.need-colors') )
@@ -2256,7 +2271,7 @@ export default class Chat extends Module {
}
- addHighlightReason(key, data) {
+ addHighlightReason(key, data, label) {
if ( typeof key === 'object' && key.key ) {
data = key;
key = data.key;
@@ -2264,16 +2279,26 @@ export default class Chat extends Module {
} else if ( typeof data === 'string' )
data = {title: data};
+ if ( typeof label === 'string' && label.length > 0 )
+ data.label = label;
+
data.value = data.key = key;
if ( ! data.i18n_key )
data.i18n_key = `hl-reason.${key}`;
+ if ( data.label && ! data.i18n_label )
+ data.i18n_label = `${data.i18n_key}.label`;
+
if ( this._hl_reasons[key] )
throw new Error(`Highlight Reason already exists with key ${key}`);
this._hl_reasons[key] = data;
}
+ getHighlightReason(key) {
+ return this._hl_reasons[key] ?? null;
+ }
+
getHighlightReasons() {
return Object.values(this._hl_reasons);
}
@@ -2560,8 +2585,10 @@ export default class Chat extends Module {
if ( (info && info[0] && refresh) || (expires && Date.now() > expires) )
info = this._link_info[url] = null;
- if ( info && info[0] )
- return no_promises ? info[2] : Promise.resolve(info[2]);
+ if ( info && info[0] ) {
+ const out = this.handleLinkToS(info[2]);
+ return no_promises ? out : Promise.resolve(out);
+ }
if ( no_promises )
return null;
@@ -2580,6 +2607,8 @@ export default class Chat extends Module {
info[1] = Date.now() + 120000;
info[2] = success ? data : null;
+ data = this.handleLinkToS(data);
+
if ( callbacks )
for(const cbs of callbacks)
cbs[success ? 0 : 1](data);
@@ -2612,6 +2641,64 @@ export default class Chat extends Module {
});
}
+
+ handleLinkToS(data) {
+ // Check for YouTube
+ const agreed = this.settings.provider.get('agreed-tos', []);
+ const resolvers = data.urls ? new Set(data.urls.map(x => x.resolver).filter(x => x)) : null;
+ if ( resolvers ) {
+ for(const [key, info] of Object.entries(RESOLVERS_REQUIRE_TOS)) {
+ if ( resolvers.has(key) && ! agreed.includes(key) ) {
+ return {
+ ...data,
+ url: null,
+ short: [
+ {
+ type: 'box',
+ content:
+ info.i18n_key
+ ? {type: 'i18n', key: info.i18n_key, phrase: info.label}
+ : info.label
+ },
+ {
+ type: 'flex',
+ 'justify-content': 'center',
+ 'align-items': 'center',
+ content: {
+ type: 'open_settings',
+ item: 'chat.tooltips'
+ }
+ }
+ ],
+ mid: null,
+ full: null
+ }
+ }
+ }
+ }
+
+ return data;
+ }
+
+
+ agreeToTerms(service) {
+ const agreed = this.settings.provider.get('agreed-tos', []);
+ if ( agreed.includes(service) )
+ return;
+
+ this.settings.provider.set('agreed-tos', [...agreed, service]);
+ this.emit(':update-link-resolver');
+ }
+
+
+ onProviderChange(key, value) {
+ if ( key !== 'agreed-tos' )
+ return;
+
+ this.emit(':update-link-resolver');
+ }
+
+
fixLinkInfo(data) {
if ( ! data )
return data;
diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx
index 85c1b1ff..66037801 100644
--- a/src/modules/chat/tokenizers.jsx
+++ b/src/modules/chat/tokenizers.jsx
@@ -110,6 +110,8 @@ export const Links = {
fragments: data.fragments,
i18n_prefix: data.i18n_prefix,
+ tooltip: true,
+
allow_media: show_images,
allow_unsafe: show_unsafe,
onload: () => requestAnimationFrame(() => tip.update())
@@ -2230,4 +2232,4 @@ export const TwitchEmotes = {
return out;
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/main_menu/components/tooltip-tos.vue b/src/modules/main_menu/components/tooltip-tos.vue
new file mode 100644
index 00000000..81b909a5
--- /dev/null
+++ b/src/modules/main_menu/components/tooltip-tos.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js
index 35f4cdb4..ee518646 100644
--- a/src/modules/main_menu/index.js
+++ b/src/modules/main_menu/index.js
@@ -250,8 +250,30 @@ export default class MainMenu extends Module {
});
this.scheduleUpdate();
+
+ this.on(':open', evt => {
+ // If we're on a page with minimal root, we want to open settings
+ // in a popout as we're almost certainly within Popout Chat.
+ const layout = this.resolve('site.layout'),
+ item = evt.item,
+ event = evt.event;
+
+ if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
+ if ( ! this.openPopout(item) )
+ evt.errored = true;
+ return;
+ }
+
+ if ( item )
+ this.requestPage(item);
+ if ( this.showing )
+ return;
+
+ this.emit('site.menu_button:clicked');
+ });
}
+
openPopout(item) {
const win = window.open(
`https://twitch.tv/popout/frankerfacez/chat?ffz-settings${item ? `=${encodeURIComponent(item)}` : ''}`,
diff --git a/src/modules/main_menu/provider-mixin.js b/src/modules/main_menu/provider-mixin.js
index b5da3b24..0ca8a0ea 100644
--- a/src/modules/main_menu/provider-mixin.js
+++ b/src/modules/main_menu/provider-mixin.js
@@ -8,13 +8,15 @@ export default {
value: undefined,
has_value: false,
+ provider_key: this.item.override_setting ?? this.item.setting,
+
_unseen: false
}
},
created() {
const provider = this.context.provider,
- setting = this.item.setting;
+ setting = this.provider_key;
provider.on('changed', this._providerChange, this);
@@ -70,7 +72,7 @@ export default {
methods: {
_providerChange(key, val, deleted) {
- if ( key !== this.item.setting )
+ if ( key !== this.provider_key )
return;
if ( deleted ) {
@@ -92,7 +94,7 @@ export default {
if ( typeof validate === 'function' )
return validate(value, this.item, this);
else
- throw new Error(`Invalid Validator for ${this.item.setting}`);
+ throw new Error(`Invalid Validator for ${this.provider_key}`);
}
return true;
@@ -100,7 +102,7 @@ export default {
set(value) {
const provider = this.context.provider,
- setting = this.item.setting;
+ setting = this.provider_key;
// TODO: Run validation.
@@ -124,7 +126,7 @@ export default {
clear() {
const provider = this.context.provider,
- setting = this.item.setting;
+ setting = this.provider_key;
provider.delete(setting);
this.value = this.default_value;
@@ -134,4 +136,4 @@ export default {
this.item.onUIChange(this.value, this);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
index 221f2626..84d9b771 100644
--- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
+++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
@@ -13,6 +13,7 @@ import Twilight from 'site';
import Module from 'utilities/module';
import SUB_STATUS from './sub_status.gql';
+import { FFZEvent } from 'src/utilities/events';
//import Tooltip from 'src/utilities/tooltip';
const TIERS = {
@@ -1419,7 +1420,15 @@ export default class EmoteMenu extends Module {
}
clickSettings(event) { // eslint-disable-line class-methods-use-this
- const layout = t.resolve('site.layout');
+ const evt = new FFZEvent({
+ item: 'chat.emote_menu',
+ event,
+ errored: false
+ });
+
+ t.emit('main_menu:open', evt);
+
+ /*const layout = t.resolve('site.layout');
if ( (layout && layout.is_minimal) || (event && (event.ctrlKey || event.shiftKey)) ) {
const win = window.open(
'https://twitch.tv/popout/frankerfacez/chat?ffz-settings',
@@ -1440,7 +1449,7 @@ export default class EmoteMenu extends Module {
}
t.emit('site.menu_button:clicked');
- }
+ }*/
}
/*clickRefresh(event) {
diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js
index 707f9ebc..9a91b3ee 100644
--- a/src/sites/twitch-twilight/modules/chat/index.js
+++ b/src/sites/twitch-twilight/modules/chat/index.js
@@ -284,6 +284,21 @@ export default class ChatHook extends Module {
// Settings
+ this.settings.add('chat.filtering.show-reasons', {
+ default: false,
+ ui: {
+ path: 'Chat > Filtering > General >> Appearance',
+ title: 'Display Reasons',
+ description: 'If this is enabled, the reasons a given message was highlighted will be displayed alongside the message. This is a simple display. Enable the debugging option below in Behavior for more details, but be aware that the debugging option has a slight performance impact compared to this.',
+ component: 'setting-select-box',
+ data: [
+ {value: false, title: 'Disabled'},
+ {value: 1, title: 'Above Message'},
+ {value: 2, title: 'Inline'}
+ ]
+ }
+ });
+
this.settings.add('chat.disable-handling', {
default: null,
requires: ['context.disable-chat-processing'],
diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js
index 6ee2cc75..e5acba64 100644
--- a/src/sites/twitch-twilight/modules/chat/line.js
+++ b/src/sites/twitch-twilight/modules/chat/line.js
@@ -927,6 +927,52 @@ other {# messages were deleted by a moderator.}
}
}
+ // Are we listing highlight reasons?
+ let highlight_tags = null;
+ const hl_position = t.chat.context.get('chat.filtering.show-reasons');
+
+ if ( msg.highlights?.size && hl_position ) {
+ highlight_tags = [];
+
+ for(const tag of msg.highlights) {
+ const reason = t.chat._hl_reasons[tag];
+ let label,
+ tooltip;
+
+ if ( reason ) {
+ tooltip = reason.i18n_key
+ ? t.i18n.t(reason.i18n_key, reason.title)
+ : reason.title;
+
+ label = reason.i18n_label
+ ? t.i18n.t(reason.i18n_label, reason.label)
+ : reason.label;
+
+ if ( label === tooltip )
+ tooltip = null;
+ }
+
+ if ( ! label )
+ label = tag;
+
+ highlight_tags.push(e('span', {
+ className: `ffz-pill ffz-highlight-tag${reason ? ' ffz-tooltip' : ''}`,
+ 'data-title': tooltip
+ }, label));
+ }
+
+ if ( highlight_tags.length > 0 )
+ highlight_tags = e('span', {
+ className: `ffz-highlight-tags ${
+ hl_position === 1
+ ? 'ffz-highlight-tags__above'
+ : 'tw-mg-r-05'
+ }`
+ }, highlight_tags);
+ else
+ highlight_tags = null;
+ }
+
// Check to see if we have message content to render.
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user),
has_message = tokens.length > 0 || ! notice;
@@ -1014,6 +1060,7 @@ other {# messages were deleted by a moderator.}
timestamp,
t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this),
this.renderInlineHighlight ? this.renderInlineHighlight() : null,
+ hl_position === 2 ? highlight_tags : null,
// Badges
e('span', {
@@ -1076,22 +1123,32 @@ other {# messages were deleted by a moderator.}
const actions = t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this);
- if ( is_raw )
- notice.ffz_target.unshift(notice.ffz_icon ?? null, timestamp, actions);
+ if ( is_raw ) {
+ notice.ffz_target.unshift(
+ notice.ffz_icon ?? null,
+ timestamp,
+ actions,
+ hl_position === 2 ? highlight_tags : null
+ );
- else
+ } else
notice = [
notice.ffz_icon ?? null,
timestamp,
actions,
+ hl_position === 2 ? highlight_tags : null,
notice
];
} else {
- if ( notice.ffz_icon )
+ if ( is_raw ) {
+ if ( notice.ffz_icon )
+ notice.ffz_target.unshift(notice.ffz_icon);
+
+ } else if ( notice.ffz_icon )
notice = [
notice.ffz_icon,
- notice
+ notice,
];
message = e(
@@ -1114,14 +1171,19 @@ other {# messages were deleted by a moderator.}
className: 'tw-c-text-alt-2'
}, notice);
- if ( message )
+ if ( highlight_tags && hl_position === 1 ) {
+ out = [highlight_tags, notice, message ?? null];
+ } else if ( message )
out = [notice, message];
else
out = notice;
} else {
klass = `${klass} chat-line__message`;
- out = message;
+ if ( highlight_tags && hl_position === 1 )
+ out = [highlight_tags, message];
+ else
+ out = message;
}
// Check for hover actions, as those require we wrap the output in a few extra elements.
@@ -1393,4 +1455,4 @@ other {# messages were deleted by a moderator.}
this.emit('chat:updated-lines');
}
-}
\ No newline at end of file
+}
diff --git a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
index 93cd7843..6d472802 100644
--- a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
+++ b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
@@ -7,6 +7,7 @@
import Twilight from 'site';
import Module from 'utilities/module';
import {createElement} from 'utilities/dom';
+import { FFZEvent } from 'src/utilities/events';
export default class SettingsMenu extends Module {
constructor(...args) {
@@ -391,6 +392,26 @@ export default class SettingsMenu extends Module {
}
click(inst, event) {
+ const target = event.currentTarget,
+ page = target && target.dataset && target.dataset.page;
+
+ const evt = new FFZEvent({
+ item: page,
+ event,
+ errored: false
+ });
+
+ this.emit('main_menu:open', evt);
+ if ( evt.errored ) {
+ this.cant_window = true;
+ this.SettingsMenu.forceUpdate();
+ return;
+ }
+
+ this.closeMenu(inst);
+ }
+
+ /*old_click(inst, event) {
// If we're on a page with minimal root, we want to open settings
// in a popout as we're almost certainly within Popout Chat.
const layout = this.resolve('site.layout');
@@ -425,5 +446,5 @@ export default class SettingsMenu extends Module {
}
this.closeMenu(inst);
- }
-}
\ No newline at end of file
+ }*/
+}
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss
index 619e6dcd..a3a8da6c 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss
@@ -2,7 +2,7 @@
.mention-fragment--recipient,
.ffz--mention-me {
border-radius: .5rem;
- padding: .3rem;
+ padding: 0 .3rem;
font-weight: 700;
color: #fff;
@@ -12,4 +12,4 @@
color: #000;
background-color: rgba(255,255,255,0.5);
}
-}
\ No newline at end of file
+}
diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss
index 553ad417..25b28930 100644
--- a/src/sites/twitch-twilight/styles/color_normalizer.scss
+++ b/src/sites/twitch-twilight/styles/color_normalizer.scss
@@ -21,6 +21,7 @@
.video-chat,
.qa-vod-chat,
.extensions-popover-view-layout,
+ .modview-dock-widget__preview__body > div,
.video-card {
background-color: var(--color-background-base) !important;
}
diff --git a/src/utilities/constants.ts b/src/utilities/constants.ts
index 492ec1b3..d002680f 100644
--- a/src/utilities/constants.ts
+++ b/src/utilities/constants.ts
@@ -134,7 +134,8 @@ export const RERENDER_SETTINGS = [
'chat.replies.style',
'chat.bits.cheer-notice',
'chat.filtering.hidden-tokens',
- 'chat.hype.message-style'
+ 'chat.hype.message-style',
+ 'chat.filtering.show-reasons'
] as const;
/**
@@ -362,3 +363,16 @@ export enum EmoteTypes {
/** Emote unlocked via following a channel. */
Follower
};
+
+
+export const RESOLVERS_REQUIRE_TOS = {
+ 'YouTube': {
+ label: 'You must agree to the YouTube Terms of Service to view this embed.',
+ i18n_key: 'embed.warn.youtube',
+
+ i18n_links: 'embed.warn.youtube.links',
+ links: `To view YouTube embeds, you must agree to YouTube's Terms of Service:
+* [Terms of Service](https://www.youtube.com/t/terms)
+* [Privacy Policy](https://policies.google.com/privacy)`
+ }
+} as Record;
diff --git a/src/utilities/rich_tokens.js b/src/utilities/rich_tokens.js
index b57dbbc0..fed7987f 100644
--- a/src/utilities/rich_tokens.js
+++ b/src/utilities/rich_tokens.js
@@ -7,6 +7,7 @@
import {has} from 'utilities/object';
import Markdown from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
+import { FFZEvent } from './events';
export const VERSION = 9;
@@ -16,7 +17,8 @@ const validate = (input, valid) => valid.includes(input) ? input : null;
const VALID_WEIGHTS = ['regular', 'bold', 'semibold'],
VALID_COLORS = ['base', 'alt', 'alt-2', 'link'],
- VALID_SIZES = ['1', '2,' ,'3','4','5','6','7','8'],
+ VALID_COLORS_TWO = ['youtube'],
+ VALID_SIZES = ['1', '2', '3', '4', '5', '6', '7', '8'],
VALID_WRAPS = ['nowrap', 'pre-wrap'],
VALID_PADDING = {
@@ -330,6 +332,49 @@ TOKEN_TYPES.box = function(token, createElement, ctx) {
}
+// ============================================================================
+// Token Type: open_settings
+// ============================================================================
+
+TOKEN_TYPES.open_settings = function(token, createElement, ctx) {
+
+ if ( ctx.tooltip )
+ return null;
+
+ const handler = event => {
+ const evt = new FFZEvent({
+ item: token.item,
+ event,
+ errored: false
+ });
+
+ window.FrankerFaceZ.get().emit('main_menu:open', evt);
+ }
+
+ const label = ctx.i18n.t('embed.show-settings', 'Open Settings');
+
+ if ( ctx.vue )
+ return createElement('button', {
+ class: 'tw-button',
+ on: {
+ click: handler
+ }
+ }, [
+ createElement('span', {
+ class: 'tw-button__text'
+ }, [
+ label
+ ])
+ ]);
+
+ return createElement('button', {
+ className: 'tw-button',
+ onClick: handler
+ }, createElement('span', {
+ className: 'tw-button__text'
+ }, label));
+}
+
// ============================================================================
// Token Type: Conditional
@@ -606,13 +651,6 @@ function header_vue(token, h, ctx) {
}, out.content));
}
- content = h('div', {
- class: [
- 'tw-flex tw-full-width tw-overflow-hidden',
- token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'
- ]
- }, content);
-
let bgtoken = resolveToken(token.sfw_background, ctx);
const nsfw_bg_token = resolveToken(token.background, ctx);
if ( nsfw_bg_token && canShowImage(nsfw_bg_token, ctx) )
@@ -632,6 +670,54 @@ function header_vue(token, h, ctx) {
background = renderWithCapture(token.background, h, ctx, token.markdown).content;
}
+ let subtok = resolveToken(token.sub_logo, ctx);
+ if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) {
+ const aspect = subtok.aspect;
+
+ let image;
+
+ if ( subtok.type === 'image' )
+ image = render_image({
+ ...subtok,
+ aspect: undefined
+ }, h, ctx);
+
+ if ( subtok.type === 'icon' )
+ image = h('figure', {
+ class: `ffz-i-${subtok.name}`
+ });
+
+ if ( image ) {
+ image = h('div', {
+ class: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.youtube_dumb ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`,
+ style: {
+ width: aspect ? `${aspect * 2}rem` : null
+ }
+ }, [image]);
+
+ const title = content.shift();
+
+ content = [
+ title,
+ h('div', {
+ class: 'tw-flex tw-full-width tw-align-items-center'
+ }, [
+ image,
+ h('div', {
+ class: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1`
+ }, content)
+ ])
+ ];
+ }
+ }
+
+ content = h('div', {
+ class: [
+ 'tw-flex tw-full-width tw-overflow-hidden',
+ token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'
+ ]
+ }, content);
+
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
@@ -759,6 +845,47 @@ function header_normal(token, createElement, ctx) {
background = renderWithCapture(token.background, createElement, ctx, token.markdown).content;
}
+ let subtok = resolveToken(token.sub_logo, ctx);
+ if ( ! token.compact && subtok && canShowImage(subtok, ctx) ) {
+ const aspect = subtok.aspect;
+
+ let image;
+
+ if ( subtok.type === 'image' )
+ image = render_image({
+ ...subtok,
+ aspect: undefined
+ }, createElement, ctx);
+
+ if ( subtok.type === 'icon' )
+ image = createElement('figure', {
+ className: `ffz-i-${subtok.name}`
+ });
+
+ if ( image ) {
+ image = createElement('div', {
+ className: `ffz--header-sublogo tw-flex-shrink-0 ${subtok.youtube_dumb ? 'tw-mg-l-05 tw-mg-r-1' : 'tw-mg-r-05'}${aspect ? ' ffz--header-aspect' : ''}`,
+ style: {
+ width: aspect ? `${aspect * 2}rem` : null
+ }
+ }, image);
+
+ const title = content.shift();
+
+ content = [
+ title,
+ createElement('div', {
+ className: 'tw-flex tw-full-width tw-align-items-center'
+ }, [
+ image,
+ createElement('div', {
+ className: `tw-flex tw-full-width tw-overflow-hidden tw-justify-content-center tw-flex-column tw-flex-grow-1`
+ }, content)
+ ])
+ ];
+ }
+ }
+
content = createElement('div', {
className: `tw-flex tw-full-width tw-overflow-hidden ${token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'}`
}, content);
@@ -1317,6 +1444,8 @@ TOKEN_TYPES.style = function(token, createElement, ctx) {
if ( token.color ) {
if ( VALID_COLORS.includes(token.color) )
classes.push(`tw-c-text-${token.color}`);
+ else if ( VALID_COLORS_TWO.includes(token.color) )
+ classes.push(`ffz-c-text-${token.color}`);
else
style.color = token.color;
}
@@ -1421,4 +1550,4 @@ TOKEN_TYPES.tag = function(token, createElement, ctx) {
...attrs,
className: token.class || ''
}, content);
-}
\ No newline at end of file
+}
diff --git a/src/utilities/time.ts b/src/utilities/time.ts
index c75503d8..00d2e6d5 100644
--- a/src/utilities/time.ts
+++ b/src/utilities/time.ts
@@ -27,7 +27,7 @@ export function duration_to_string(
days = day_count > 0 ? `${day_count} days, ` : '';
}
- const show_hours = (!no_hours || days || hours);
+ const show_hours = (no_hours === false || days?.length > 0 || hours > 0);
return `${days}${
show_hours ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''
@@ -68,4 +68,4 @@ export function durationForURL(elapsed: number) {
minutes = minutes % 60;
return `${hours > 0 ? `${hours}h` : ''}${minutes > 0 ? `${minutes}m` : ''}${seconds > 0 ? `${seconds}s` : ''}`;
-}
\ No newline at end of file
+}
diff --git a/styles/chat.scss b/styles/chat.scss
index c99358c7..71c3ae2b 100644
--- a/styles/chat.scss
+++ b/styles/chat.scss
@@ -55,6 +55,12 @@
}
}
+.ffz--rich-header div span[class="ffz-i-youtube-play"] {
+ font-size: 166%;
+ line-height: 1em;
+ vertical-align: middle;
+}
+
.ffz__tooltip {
--ffz-rich-header-outline: var(--color-background-tooltip);
.ffz--rich-header--background {
@@ -118,6 +124,15 @@
> * {
width: 100%;
}
+
+ .ffz--overlay .ffz--overlay__bit[data-side=center] {
+ border-radius: 50%;
+ aspect-ratio: 1;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
}
.ffz--overlay {
@@ -283,6 +298,28 @@
}
}
+
+.ffz-highlight-tags__above {
+ display: block;
+ margin-top: -0.5rem;
+}
+
+.ffz-highlight-tag:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ffz-highlight-tag + .ffz-highlight-tag {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.ffz-highlight-tag {
+ margin-right: 2px;
+ background: rgba(127,127,127,0.35);
+ color: inherit;
+}
+
.ffz--fields {
display: flex;
margin-top: -.5rem;
diff --git a/styles/main.scss b/styles/main.scss
index 0cc4607a..7fa545e0 100644
--- a/styles/main.scss
+++ b/styles/main.scss
@@ -39,6 +39,15 @@
display: inline;
}
+.ffz__tooltip .ffz-c-text-youtube,
+.tw-root--theme-dark .ffz-c-text-youtube {
+ color: #fff !important;
+}
+.tw-root--theme-dark .ffz__tooltip .ffz-c-text-youtube,
+.ffz-c-text-youtube {
+ color: #212121 !important;
+}
+
.ffz--player-meta-tray {
position: absolute;
bottom: 100%;
@@ -152,4 +161,4 @@
.ffz-aspect--align-bottom > :not(.ffz-aspect__spacer) {
bottom: 0;
-}
\ No newline at end of file
+}
diff --git a/styles/native/index.scss b/styles/native/index.scss
index 6b5345a9..dad74ad0 100644
--- a/styles/native/index.scss
+++ b/styles/native/index.scss
@@ -244,6 +244,10 @@
display: flex !important;
}
+.tw-flex-inline {
+ display: inline-flex !important;
+}
+
.tw-flex-wrap {
flex-wrap: wrap !important;
}
@@ -2728,4 +2732,4 @@
.tw-core-button-label--dropdown {
padding-right: .8rem;
padding-right: calc(var(--button-padding-x) - .2rem)
-}
\ No newline at end of file
+}