diff --git a/README.md b/README.md index 6b7767ff..ead1450a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ FrankerFaceZ comes with a local development server that listens on port 8000 and it serves up local development copies of files, falling back to the CDN when a local copy of a file isn't present. +> **Note:** The local development server uses `webpack-dev-server` internally, +> which self-signs a certificate for hosting content via HTTPS. You will need +> to ensure your browser accepts a self-signed certificate for localhost. + To make FrankerFaceZ load from your local development server, you must set the local storage variable `ffzDebugMode` to true. Just run the following in your console on Twitch: `localStorage.ffzDebugMode = true;` diff --git a/fontello.config.json b/fontello.config.json index 1aa36f85..b2c3e9d1 100644 --- a/fontello.config.json +++ b/fontello.config.json @@ -735,6 +735,24 @@ "css": "chat", "code": 59457, "src": "fontawesome" + }, + { + "uid": "0d08dbb1dd648a43bdea81b7e6c9e036", + "css": "location", + "code": 59458, + "src": "fontawesome" + }, + { + "uid": "0ddd3e8201ccc7d41f7b7c9d27eca6c1", + "css": "link", + "code": 59459, + "src": "fontawesome" + }, + { + "uid": "8489d61496923b1159b01d8a0a7b2df0", + "css": "volume-off", + "code": 59461, + "src": "elusive" } ] } \ No newline at end of file diff --git a/package.json b/package.json index 4e6b8c75..5a621737 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.21", + "version": "4.20.22", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/res/font/ffz-fontello.eot b/res/font/ffz-fontello.eot index 92d306ab..cd269901 100644 Binary files a/res/font/ffz-fontello.eot and b/res/font/ffz-fontello.eot differ diff --git a/res/font/ffz-fontello.svg b/res/font/ffz-fontello.svg index 99c104c8..63608d1f 100644 --- a/res/font/ffz-fontello.svg +++ b/res/font/ffz-fontello.svg @@ -138,6 +138,12 @@ + + + + + + diff --git a/res/font/ffz-fontello.ttf b/res/font/ffz-fontello.ttf index e33dfde7..fa3537ba 100644 Binary files a/res/font/ffz-fontello.ttf and b/res/font/ffz-fontello.ttf differ diff --git a/res/font/ffz-fontello.woff b/res/font/ffz-fontello.woff index 53c13fba..604d617f 100644 Binary files a/res/font/ffz-fontello.woff and b/res/font/ffz-fontello.woff differ diff --git a/res/font/ffz-fontello.woff2 b/res/font/ffz-fontello.woff2 index 95beb84c..2c36f996 100644 Binary files a/res/font/ffz-fontello.woff2 and b/res/font/ffz-fontello.woff2 differ diff --git a/src/addons.js b/src/addons.js index 39e53007..d47a120e 100644 --- a/src/addons.js +++ b/src/addons.js @@ -37,7 +37,7 @@ export default class AddonManager extends Module { this._loader = this.loadAddonData(); } - async onEnable() { + onEnable() { this.settings.addUI('add-ons', { path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch.", "profile_warning": false}', component: 'addon-list', diff --git a/src/i18n.js b/src/i18n.js index 8f02e2c8..f9a9b6ab 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -163,7 +163,7 @@ export class TranslationManager extends Module { }, ui: { - path: 'Appearance > Localization >> General', + path: 'Appearance > Localization >> General @{"sort":-100}', title: 'Language', description: `FrankerFaceZ is lovingly translated by volunteers from our community. Thank you. If you're interested in helping to translate FrankerFaceZ, please [join our Discord](https://discord.gg/UrAkGhT) and ask about localization.`, @@ -173,6 +173,82 @@ export class TranslationManager extends Module { changed: val => this.locale = val }); + + + this.settings.add('i18n.format.date', { + default: 'default', + ui: { + path: 'Appearance > Localization >> Formatting', + title: 'Date Format', + description: 'The default date format. Custom date formats are formated using the [Day.js](https://github.com/iamkun/dayjs#readme) library.', + component: 'setting-combo-box', + data: () => { + const out = [], now = new Date; + for (const [key,fmt] of Object.entries(this._.formats.date)) { + out.push({ + value: key, title: `${this.formatDate(now, key)} (${key})` + }) + } + + return out; + } + }, + + changed: val => { + this._.defaultDateFormat = val; + this.emit(':update') + } + }); + + this.settings.add('i18n.format.time', { + default: 'short', + ui: { + path: 'Appearance > Localization >> Formatting', + title: 'Time Format', + description: 'The default time format. Custom time formats are formated using the [Day.js](https://github.com/iamkun/dayjs#readme) library.', + component: 'setting-combo-box', + data: () => { + const out = [], now = new Date; + for (const [key,fmt] of Object.entries(this._.formats.time)) { + out.push({ + value: key, title: `${this.formatTime(now, key)} (${key})` + }) + } + + return out; + } + }, + + changed: val => { + this._.defaultTimeFormat = val; + this.emit(':update') + } + }); + + this.settings.add('i18n.format.datetime', { + default: 'medium', + ui: { + path: 'Appearance > Localization >> Formatting', + title: 'Date-Time Format', + description: 'The default combined date-time format. Custom time formats are formated using the [Day.js](https://github.com/iamkun/dayjs#readme) library.', + component: 'setting-combo-box', + data: () => { + const out = [], now = new Date; + for (const [key,fmt] of Object.entries(this._.formats.datetime)) { + out.push({ + value: key, title: `${this.formatDateTime(now, key)} (${key})` + }) + } + + return out; + } + }, + + changed: val => { + this._.defaultDateTimeFormat = val; + this.emit(':update') + } + }); } getLocaleOptions(val) { @@ -244,6 +320,9 @@ export class TranslationManager extends Module { this._ = new NewTransCore({ //TranslationCore({ warn: (...args) => this.log.warn(...args), + defaultDateFormat: this.settings.get('i18n.format.date'), + defaultTimeFormat: this.settings.get('i18n.format.time'), + defaultDateTimeFormat: this.settings.get('i18n.format.datetime') }); if ( window.BroadcastChannel ) { diff --git a/src/modules/chat/clip_info.gql b/src/modules/chat/clip_info.gql index 28b09544..c6607910 100644 --- a/src/modules/chat/clip_info.gql +++ b/src/modules/chat/clip_info.gql @@ -3,10 +3,12 @@ query FFZ_GetClipInfo($slug: ID!) { id curator { id + login displayName } broadcaster { id + login displayName } game { diff --git a/src/modules/chat/components/chat-rich.vue b/src/modules/chat/components/chat-rich.vue index eee31bc3..ee2257f2 100644 --- a/src/modules/chat/components/chat-rich.vue +++ b/src/modules/chat/components/chat-rich.vue @@ -1,27 +1,39 @@ \ No newline at end of file diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 24fa9af9..bc541a1b 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -24,6 +24,7 @@ import Actions from './actions'; export const SEPARATORS = '[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]'; +const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0'; const EMOTE_CHARS = /[ .,!]/; export default class Chat extends Module { @@ -1411,7 +1412,7 @@ export default class Chat extends Module { const tt = tokenizer.tooltip; const tk = this.tooltips.types[type] = tt.bind(this); - for(const i of ['interactive', 'delayShow', 'delayHide']) + for(const i of ['interactive', 'delayShow', 'delayHide', 'onShow', 'onHide']) tk[i] = typeof tt[i] === 'function' ? tt[i].bind(this) : tt[i]; } @@ -1579,6 +1580,8 @@ export default class Chat extends Module { info = this._link_info[url] = [false, null, [[resolve, reject]]]; const handle = (success, data) => { + data = this.fixLinkInfo(data); + const callbacks = ! info[0] && info[2]; info[0] = true; info[1] = Date.now() + 120000; @@ -1608,4 +1611,43 @@ export default class Chat extends Module { } }); } + + fixLinkInfo(data) { + if ( data.error && data.message ) + data.error = data.message; + + if ( data.error ) + data = { + v: 5, + title: this.i18n.t('card.error', 'An error occured.'), + description: data.error, + short: { + type: 'header', + image: {type: 'image', url: ERROR_IMAGE}, + title: {type: 'i18n', key: 'card.error', phrase: 'An error occured.'}, + subtitle: data.error + } + } + + if ( data.v < 5 && ! data.short && ! data.full && (data.title || data.desc_1 || data.desc_2) ) { + const image = data.preview || data.image; + + data = { + v: 5, + short: { + type: 'header', + image: image ? { + type: 'image', + url: image, + sfw: data.image_safe ?? false, + } : null, + title: data.title, + subtitle: data.desc_1, + extra: data.desc_2 + } + } + } + + return data; + } } \ No newline at end of file diff --git a/src/modules/chat/rich_providers.js b/src/modules/chat/rich_providers.js index b5db9f30..fcac0399 100644 --- a/src/modules/chat/rich_providers.js +++ b/src/modules/chat/rich_providers.js @@ -17,6 +17,8 @@ const BAD_USERS = [ import GET_CLIP from './clip_info.gql'; import GET_VIDEO from './video_info.gql'; +import {truncate} from 'utilities/object'; + // ============================================================================ // General Links @@ -47,31 +49,20 @@ export const Links = { } catch(err) { return { url: token.url, - title: this.i18n.t('card.error', 'An error occurred.'), - desc_1: String(err) + error: String(err) } } if ( ! data ) return { - url: token.url, - title: this.i18n.t('card.error', 'An error occurred.'), - desc_1: this.i18n.t('card.empty', 'No data was returned.') + url: token.url } return { - url: token.url, - accent: data.accent, - image: this.context.get('tooltip.link-images') ? (data.image_safe || this.context.get('tooltip.link-nsfw-images') ) ? data.preview || data.image : null : null, - image_title: data.image_title, - image_square: data.image_square, - title: data.title, - title_tokens: data.title_tokens, - desc_1: data.desc_1, - desc_1_tokens: data.desc_1_tokens, - desc_2: data.desc_2, - desc_2_tokens: data.desc_2_tokens - } + ...data, + allow_media: this.context.get('tooltip.link-images'), + allow_unsafe: this.context.get('tooltip.link-nsfw-images') + }; } } } @@ -108,66 +99,82 @@ export const Users = { if ( ! user || ! user.id ) return null; - const game = user.broadcastSettings?.game?.displayName; + const game = user.broadcastSettings?.game?.displayName, + stream_id = user.stream?.id; - let desc_1 = null, desc_2 = null, desc_1_tokens = null, desc_2_tokens = null; - if ( user.stream?.id && game ) { - desc_1_tokens = this.i18n.tList('cards.user.streaming', 'streaming {game}', { - game: {class: 'tw-semibold', content: [game]} - }); - desc_1 = this.i18n.t('cards.user.streaming', 'streaming {game}', { - game - }); - } + let subtitle + if ( stream_id && game ) + subtitle = { + type: 'i18n', + key: 'cards.user.streaming', phrase: 'streaming {game}', content: { + game: {type: 'style', weight: 'semibold', content: game} + } + }; - const bits_tokens = this.i18n.tList('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', { - views: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.profileViewCount || 0)]}, - followers: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.followers?.totalCount || 0)]} - }), - bits = this.i18n.t('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', { - views: user.profileViewCount || 0, - followers: user.followers?.totalCount || 0 + const extra = truncate(user.description); + const title = [user.displayName]; + + if ( user.displayName.trim().toLowerCase() !== user.login ) + title.push({ + type: 'style', color: 'alt-2', + content: [' (', user.login, ')'] }); - if ( desc_1 ) { - desc_2 = bits; - desc_2_tokens = bits_tokens; - } else { - desc_1 = bits; - desc_1_tokens = bits_tokens; - } + if ( user.roles?.isPartner ) + title.push({ + type: 'style', color: 'link', + content: {type: 'icon', name: 'verified'} + }); - const has_i18n = user.displayName.trim().toLowerCase() !== user.login; - let title = user.displayName, title_tokens = null; - if ( has_i18n ) { - title = `${user.displayName} (${user.login})`; - title_tokens = [ - user.displayName, - {class: 'chat-author__intl-login', content: ` (${user.login})`} - ]; - } + /*const full = [{ + type: 'header', + image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1}, + title, + subtitle, + extra: stream_id ? extra : null + }]; - if ( user.roles?.isPartner ) { - if ( ! title_tokens ) - title_tokens = [title]; + if ( stream_id ) { + full.push({type: 'box', 'mg-y': 'small', lines: 1, content: user.broadcastSettings.title}); + full.push({type: 'conditional', content: { + type: 'gallery', items: [{ + type: 'image', aspect: 16/9, sfw: false, url: user.stream.previewImageURL + }] + }}); + } else + full.push({type: 'box', 'mg-y': 'small', wrap: 'pre-wrap', lines: 5, content: truncate(user.description, 1000, undefined, undefined, false)}) - title_tokens = {tag: 'div', class: 'tw-flex tw-align-items-center', content: [ - {tag: 'div', content: title_tokens}, - {tag: 'figure', class: 'tw-mg-l-05 ffz-i-verified tw-c-text-link', content: []} - ]}; - } + full.push({ + type: 'fieldset', + fields: [ + { + name: {type: 'i18n', key: 'embed.twitch.views', phrase: 'Views'}, + value: {type: 'format', format: 'number', value: user.profileViewCount}, + inline: true + }, + { + name: {type: 'i18n', key: 'embed.twitch.followers', phrase: 'Followers'}, + value: {type: 'format', format: 'number', value: user.followers?.totalCount}, + inline: true + } + ] + }); + + full.push({ + type: 'header', + subtitle: [{type: 'icon', name: 'twitch'}, ' Twitch'] + });*/ return { url: token.url, accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null, - image: user.profileImageURL, - image_square: true, - title, - title_tokens, - desc_1, - desc_1_tokens, - desc_2, - desc_2_tokens + short: { + type: 'header', + image: {type: 'image', url: user.profileImageURL, rounding: -1, aspect: 1}, + title, + subtitle, + extra + } } } } @@ -214,53 +221,51 @@ export const Clips = { return null; const clip = result.data.clip, - user = clip.broadcaster.displayName, game = clip.game, - game_name = game && game.name, game_display = game && game.displayName; - let desc_1, desc_1_tokens; - if ( game_name === 'creative' ) { - desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', { - user: {class: 'tw-semibold', content: user} - }); - desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', { - user - }); + const user = { + type: 'link', url: `https://www.twitch.tv/${clip.broadcaster.login}`, + content: { + type: 'style', weight: 'semibold', color: 'alt-2', + content: clip.broadcaster.displayName + } + }; - } else if ( game ) { - desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', { - user: {class: 'tw-semibold', content: user}, - game: {class: 'tw-semibold', content: game_display} - }); - desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', { + const subtitle = game_display ? { + type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: { user, - game: game_display - }); + game: {type: 'style', weight: 'semibold', content: game_display} + } + } : {type: 'i18n', key: 'clip.desc.1', phrase: 'Clip of {user}', content: {user}}; - } else { - desc_1_tokens = this.i18n.tList('clip.desc.1', 'Clip of {user}', { - user: {class: 'tw-semibold', content: user} - }); - desc_1 = this.i18n.t('clip.desc.1', 'Clip of {user}', {user}); - } + const curator = clip.curator ? { + type: 'link', url: `https://www.twitch.tv/${clip.curator.login}`, + content: { + type: 'style', color: 'alt-2', + content: clip.curator.displayName + } + } : {type: 'i18n', key: 'clip.unknown', phrase: 'Unknown'}; - const curator = clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown'); + const extra = { + type: 'i18n', key: 'clip.desc.2', + phrase: 'Clipped by {curator} — {views,number} View{views,en_plural}', + content: { + curator, + views: clip.viewCount + } + }; return { url: token.url, - image: clip.thumbnailURL, - title: clip.title, - desc_1, - desc_1_tokens, - desc_2: this.i18n.t('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', { - curator, - views: clip.viewCount - }), - desc_2_tokens: this.i18n.tList('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', { - curator: clip.curator ? {class: 'tw-semibold', content: curator} : curator, - views: {class: 'tw-semibold', content: this.i18n.formatNumber(clip.viewCount)} - }) + + short: { + type: 'header', + image: {type: 'image', url: clip.thumbnailURL, sfw: false, aspect: 16/9}, + title: clip.title, + subtitle, + extra + } } } } @@ -296,49 +301,43 @@ export const Videos = { return null; const video = result.data.video, - user = video.owner.displayName, game = video.game, - game_name = game && game.name, game_display = game && game.displayName; - let desc_1, desc_1_tokens; - if ( game_name === 'creative' ) { - desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', { - user: {class: 'tw-semibold', content: user} - }); - desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', { - user - }); + const user = { + type: 'link', url: `https://www.twitch.tv/${video.owner.login}`, + content: { + type: 'style', weight: 'semibold', color: 'alt-2', + content: video.owner.displayName + } + }; - } else if ( game ) { - desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', { - user: {class: 'tw-semibold', content: user}, - game: {class: 'tw-semibold', content: game_display} - }); - desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', { + const subtitle = game_display ? { + type: 'i18n', key: 'clip.desc.1.playing', phrase: '{user} playing {game}', content: { user, - game: game_display - }); + game: {type: 'style', weight: 'semibold', content: game_display} + } + } : {type: 'i18n', key: 'video.desc.1', phrase: 'Video of {user}', content: {user}}; - } else { - desc_1_tokens = this.i18n.tList('video.desc.1', 'Video of {user}', { - user: {class: 'tw-semibold', content: user} - }); - desc_1 = this.i18n.t('video.desc.1', 'Video of {user}', {user}); - } - - return { - url: token.url, - image: video.previewThumbnailURL, - title: video.title, - desc_1, - desc_1_tokens, - desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date,datetime}', { + const extra = { + type: 'i18n', key: 'video.desc.2', + phrase: '{length,duration} — {views,number} Views — {date,datetime}', content: { length: video.lengthSeconds, views: video.viewCount, date: video.publishedAt - }) - } + } + }; + + return { + url: token.url, + short: { + type: 'header', + image: {type: 'image', url: video.previewThumbnailURL, sfw: false, aspect: 16/9}, + title: video.title, + subtitle, + extra + } + }; } } } diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 7bfa94f6..29e930cc 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -20,7 +20,11 @@ const EMOTE_CLASS = 'chat-image chat-line__message--emote', // Links // ============================================================================ -const TOOLTIP_VERSION = 4; +function datasetBool(value) { + return value == null ? null : value === 'true'; +} + +const TOOLTIP_VERSION = 5; export const Links = { type: 'link', @@ -47,60 +51,94 @@ export const Links = { if ( target.dataset.isMail === 'true' ) return [this.i18n.t('tooltip.email-link', 'E-Mail {address}', {address: target.textContent})]; - const url = target.dataset.url || target.href; + const url = target.dataset.url || target.href, + show_images = datasetBool(target.dataset.forceMedia) ?? this.context.get('tooltip.link-images'), + show_unsafe = datasetBool(target.dataset.forceUnsafe) ?? this.context.get('tooltip.link-nsfw-images'); - return this.get_link_info(url).then(data => { + return Promise.all([ + import(/* webpack-chunk-name: 'rich_tokens' */ 'utilities/rich_tokens'), + this.get_link_info(url) + ]).then(([rich_tokens, data]) => { if ( ! data || (data.v || 1) > TOOLTIP_VERSION ) return ''; - let content = data.content || data.html || ''; + const ctx = { + tList: (...args) => this.i18n.tList(...args), + i18n: this.i18n, + allow_media: show_images, + allow_unsafe: show_unsafe, + onload: tip.update + }; - // TODO: Replace timestamps. - - if ( data.urls && data.urls.length > 1 ) - content += (content.length ? '
' : '') + - sanitize(this.i18n.t( - 'tooltip.link-destination', - 'Destination: {url}', - {url: data.urls[data.urls.length-1][1]} - )); - - if ( data.unsafe ) { - const reasons = Array.from(new Set(data.urls.map(x => x[2]).filter(x => x))).join(', '); - content = this.i18n.t( - 'tooltip.link-unsafe', - "Caution: This URL is on Google's Safe Browsing List for: {reasons}", - {reasons: sanitize(reasons.toLowerCase())} - ) + (content.length ? `
${content}` : ''); + let content; + if ( tip.element ) { + tip.element.classList.add('ffz-rich-tip'); + tip.element.classList.add('tw-align-left'); } - const show_image = this.context.get('tooltip.link-images') && (data.image_safe || this.context.get('tooltip.link-nsfw-images')); + if ( data.full ) { + content = rich_tokens.renderTokens(data.full, createElement, ctx); - if ( show_image ) { - if ( data.image && ! data.image_iframe ) - content = `${content}` + } else { + if ( data.short ) { + content = rich_tokens.renderTokens(data.short, createElement, ctx); + } else + content = this.i18n.t('card.empty', 'No data was returned.'); + } - setTimeout(() => { - if ( tip.element ) { - for(const el of tip.element.querySelectorAll('img')) - el.addEventListener('load', tip.update); + if ( ! data.urls ) + return content; - for(const el of tip.element.querySelectorAll('video')) - el.addEventListener('loadedmetadata', tip.update); - } + const url_table = []; + for(let i=0; i < data.urls.length; i++) { + const url = data.urls[i]; + + url_table.push( + {this.i18n.formatNumber(i + 1)}. + {url.url} + {url.flags ? url.flags.map(flag => {flag.toLowerCase()}) : null} + ); + } + + let url_notice; + if ( data.unsafe ) { + const reasons = Array.from(new Set(data.urls.map(url => url.flags).flat())).join(', '); + url_notice = (
+ {this.i18n.tList( + 'tooltip.link-unsafe', + "Caution: This URL is on Google's Safe Browsing List for: {reasons}", + {reasons: reasons.toLowerCase()} + )} +
); + } else if ( data.urls.length > 1 ) + url_notice = this.i18n.t('tooltip.link-destination', 'Destination: {url}', { + url: data.urls[data.urls.length-1].url }); - } else if ( content.length ) - content = content.replace(/.*/g, ''); - - if ( data.tooltip_class ) - tip.element.classList.add(data.tooltip_class); + content = (
+
+ {content} + {url_notice ?
+ {url_notice} +
+ {this.i18n.t('tooltip.shift-detail', '(Shift for Details)')} +
+
: null} +
+
+
+ {this.i18n.t('tooltip.link.urls', 'Visited URLs')} +
+ {url_table}
+
+
); return content; - }).catch(error => - sanitize(this.i18n.t('tooltip.error', 'An error occurred. ({error})', {error})) - ); + }).catch(error => { + console.error(error); + return sanitize(this.i18n.t('tooltip.error', 'An error occurred. ({error})', {error})) + }); }, process(tokens) { diff --git a/src/modules/chat/video_info.gql b/src/modules/chat/video_info.gql index 919bdde5..04fe9f45 100644 --- a/src/modules/chat/video_info.gql +++ b/src/modules/chat/video_info.gql @@ -12,6 +12,7 @@ query FFZ_GetVideoInfo($id: ID!) { } owner { id + login displayName } } diff --git a/src/modules/main_menu/components/addon.vue b/src/modules/main_menu/components/addon.vue index ee248edf..4d518d47 100644 --- a/src/modules/main_menu/components/addon.vue +++ b/src/modules/main_menu/components/addon.vue @@ -104,7 +104,8 @@ v-if="addon.website" :href="addon.website" :title="addon.website" - class="tw-button ffz-button--hollow tw-mg-r-1" + class="tw-button ffz-button--hollow tw-mg-r-1 ffz-tooltip ffz-tooltip--no-mouse" + data-tooltip-type="link" target="_blank" rel="noopener" > diff --git a/src/modules/main_menu/components/changelog.vue b/src/modules/main_menu/components/changelog.vue index 3d9a42db..8610f8ef 100644 --- a/src/modules/main_menu/components/changelog.vue +++ b/src/modules/main_menu/components/changelog.vue @@ -53,7 +53,8 @@ :href="commit.author.html_url" target="_blank" rel="noopener noreferrer" - class="tw-inline-flex tw-align-items-center tw-link tw-link--inherit tw-mg-x-05" + class="tw-inline-flex tw-align-items-center tw-link tw-link--inherit tw-mg-x-05 ffz-tooltip" + data-tooltip-type="link" >
- @{{ commit.hash }} + @{{ commit.hash }}