From 6e78fd7caba59e4237853257e2006973c7e3c4ab Mon Sep 17 00:00:00 2001 From: SirStendec Date: Tue, 19 Dec 2023 16:24:33 -0500 Subject: [PATCH] 4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled. --- package.json | 2 +- src/modules/chat/components/chat-rich.vue | 4 +- src/modules/chat/index.js | 105 +++++++++++-- src/modules/chat/tokenizers.jsx | 4 +- .../main_menu/components/tooltip-tos.vue | 67 ++++++++ src/modules/main_menu/index.js | 22 +++ src/modules/main_menu/provider-mixin.js | 14 +- .../modules/chat/emote_menu.jsx | 13 +- .../twitch-twilight/modules/chat/index.js | 15 ++ .../twitch-twilight/modules/chat/line.js | 78 +++++++++- .../modules/chat/settings_menu.jsx | 25 ++- .../css_tweaks/styles/chat-mention-token.scss | 4 +- .../styles/color_normalizer.scss | 1 + src/utilities/constants.ts | 16 +- src/utilities/rich_tokens.js | 147 ++++++++++++++++-- src/utilities/time.ts | 4 +- styles/chat.scss | 37 +++++ styles/main.scss | 11 +- styles/native/index.scss | 6 +- 19 files changed, 528 insertions(+), 47 deletions(-) create mode 100644 src/modules/main_menu/components/tooltip-tos.vue 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 +}