From cbbe6f3e2027f643ff14b90717bed1fea2d450d8 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Tue, 28 Nov 2017 02:03:59 -0500 Subject: [PATCH] Basic badge stuff. Rendering of extension badges is still disabled for now, but SOON(tm). Fix emotes in locally echoed messages when emoji are present. Cache tokens in chat lines. Include the intl-login element within the username link in chat so it all underlines together. --- src/modules/chat/badges.js | 401 +++++++++++++++++- src/modules/chat/index.js | 94 +--- src/modules/chat/room.js | 31 +- src/modules/chat/user.js | 51 ++- src/modules/tooltips.js | 31 -- .../twitch-twilight/modules/chat/index.js | 19 +- .../twitch-twilight/modules/chat/line.js | 23 +- .../twitch-twilight/modules/chat/scroller.js | 2 +- .../twitch-twilight/modules/theme/index.js | 7 + src/utilities/constants.js | 4 +- src/utilities/object.js | 3 + styles/tooltips.scss | 6 + 12 files changed, 517 insertions(+), 155 deletions(-) diff --git a/src/modules/chat/badges.js b/src/modules/chat/badges.js index c7f5b80e..49ae9f06 100644 --- a/src/modules/chat/badges.js +++ b/src/modules/chat/badges.js @@ -4,24 +4,409 @@ // Badge Handling // ============================================================================ +import {API_SERVER, WEBKIT_CSS as WEBKIT} from 'utilities/constants'; + +import {createElement as e, ManagedStyle} from 'utilities/dom'; +import {has} from 'utilities/object'; import Module from 'utilities/module'; export const CSS_BADGES = { - staff: { 1: { color: '#200f33', use_svg: true } }, - admin: { 1: { color: '#faaf19', use_svg: true } }, - global_mod: { 1: { color: '#0c6f20', use_svg: true } }, - broadcaster: { 1: { color: '#e71818', use_svg: true } }, - moderator: { 1: { color: '#34ae0a', use_svg: true } }, + staff: { 1: { color: '#200f33', svg: true, trans: { color: '#6441a5' } } }, + admin: { 1: { color: '#faaf19', svg: true } }, + global_mod: { 1: { color: '#0c6f20', svg: true } }, + broadcaster: { 1: { color: '#e71818', svg: true } }, + moderator: { 1: { color: '#34ae0a', svg: true } }, twitchbot: { 1: { color: '#34ae0a' } }, - partner: { 1: { color: 'transparent', has_trans: true, trans_color: '#6441a5' } }, + partner: { 1: { color: 'transparent', trans: { image: true, color: '#6441a5' } } }, - turbo: { 1: { color: '#6441a5', use_svg: true } }, + turbo: { 1: { color: '#6441a5', svg: true } }, premium: { 1: { color: '#009cdc' } }, subscriber: { 0: { color: '#6441a4' }, 1: { color: '#6441a4' }}, } -export default class Badges extends Module { +const NO_REPEAT = 'background-repeat:no-repeat;background-position:center;', + BASE_IMAGE = 'https://cdn.frankerfacez.com/badges/twitch/', + CSS_TEMPLATES = { + 0: data => `background:${data.image} ${data.color};${data.svg ? `background-size:${data.scale*18}px;` : `background-image:${data.image_set};`}${NO_REPEAT}`, + 1: data => `${CSS_TEMPLATES[0](data)}border-radius:${data.scale*2}px;`, + 2: data => `${CSS_TEMPLATES[0](data)}border-radius:${data.scale*9}px;background-size:${data.scale*16}px;`, + 3: data => `background:${data.color};border-radius:${data.scale*9}px;`, + 4: data => `${CSS_TEMPLATES[3](data)}height:${data.scale*10}px;min-width:${data.scale*10}px;`, + 5: data => `background:${data.image};${data.svg ? `background-size:${data.scale*18}px;` : `background-image:${data.image_set};`}${NO_REPEAT}`, + 6: data => `background:linear-gradient(${data.color},${data.color});${WEBKIT}mask-image:${data.image};${data.svg ? `${WEBKIT}mask-size:${data.scale*18}px ${data.scale*18}px;` : `${WEBKIT}mask-image:${data.image_set};`}` + }; + +export function generateBadgeCSS(badge, version, data, style, is_dark, scale = 1) { + let color = data.color || 'transparent', + base_image = data.image || `${BASE_IMAGE}${badge}${data.svg ? '.svg' : `/${version}/`}`, + trans = false, + invert = false, + svg, image, image_set; + + if ( style > 4 ) { + const td = data.trans || {}; + color = td.color || color; + + if ( td.image ) { + trans = true; + if ( td.image !== true ) + base_image = td.image; + + } + + if ( has(td, 'invert') ) + invert = td.invert && ! is_dark; + else + invert = style === 5 && ! is_dark; + } + + if ( style === 3 || style === 4 ) { + if ( color === 'transparent' && data.trans ) + color = data.trans.color || color; + + } else { + if ( style < 5 && color === 'transparent' ) + style = 0; + + svg = base_image.endsWith('.svg'); + image = `url("${svg ? base_image : `${base_image}${scale}${trans ? '_trans' : ''}.png`}")`; + + if ( svg && scale < 4 ) { + if ( scale === 1 ) + image_set = `${WEBKIT}image-set(${image} 1x, url("${base_image}2${trans ? '_trans' : ''}.png") 2x, url("${base_image}4${trans ? '_trans' : ''}.png") 4x)`; + + else if ( scale === 2 ) + image_set = `${WEBKIT}image-set(${image} 1x, url("${base_image}4${trans ? '_trans' : ''}.png") 2x)`; + + } else + image_set = svg; + } + + return `${invert ? 'filter:invert(100%);' : ''}${CSS_TEMPLATES[style]({ + scale, + color, + image, + image_set, + svg + })}`; +} + + +export default class Badges extends Module { + constructor(...args) { + super(...args); + + this.inject('i18n'); + this.inject('settings'); + this.inject('socket'); + this.inject('tooltips'); + + this.style = new ManagedStyle('badges'); + this.badges = {}; + + this.twitch_badges = new Map; + + this.settings.add('chat.badges.style', { + default: 0, + ui: { + path: 'Chat > Badges >> Appearance', + title: 'Style', + component: 'setting-select-box', + data: [ + {value: 0, title: 'Default'}, + {value: 1, title: 'Rounded'}, + {value: 2, title: 'Circular'}, + {value: 3, title: 'Circular (Color Only)'}, + {value: 4, title: 'Circular (Color Only, Small)'}, + {value: 5, title: 'Transparent'}, + {value: 6, title: 'Transparent (Colored)'} + ] + } + }); + } + + + onEnable() { + this.parent.context.on('changed:chat.badges.style', this.rebuildAllCSS, this); + this.parent.context.on('changed:theme.is-dark', this.rebuildAllCSS, this); + this.parent.context.on('changed:theme.tooltips-dark', this.rebuildAllCSS, this); + + this.rebuildAllCSS(); + this.loadGlobalBadges(); + + this.tooltips.types.badge = (target, tip) => { + const container = target.parentElement.parentElement, + provider = target.dataset.provider, + badge = target.dataset.badge, + version = target.dataset.version, + room_id = container.dataset.roomId, + room_login = container.dataset.room; + + let preview, title; + + if ( provider === 'twitch' ) { + const data = this.getTwitchBadge(badge, version, room_id, room_login); + if ( ! data ) + return; + + preview = data.image4x; + title = data.title; + + } else if ( provider === 'ffz' ) { + const data = this.badges[badge]; + if ( ! data ) + return; + + preview = e('span', { + className: 'preview-image ffz-badge', + style: { + height: '72px', + width: '72px', + backgroundColor: data.color, + backgroundImage: `url("${data.urls[4]}")` + } + }) + + title = this.i18n.t(`badges.${data.name}`, data.title); + + } else + title = `Unknown Badge`; + + if ( preview ) + if ( this.parent.context.get('tooltip.badge-images') ) { + if ( typeof preview === 'string' ) + preview = e('img', { + className: 'preview-image ffz-badge', + src: preview, + }); + + } else + preview = null; + + return [ + preview, + title + ]; + }; + } + + + render(msg, e) { // eslint-disable-line class-methods-use-this + const out = [], + twitch_badges = msg.badges || {}; + + /*user = msg.user || {}, + user_id = user.userID, + user_login = user.userLogin, + room_id = msg.roomID, + room_login = msg.roomLogin, + + badges = this.getBadges(user_id, user_login, room_id, room_login);*/ + + for(const badge_id in twitch_badges) + if ( has(twitch_badges, badge_id) ) { + const version = twitch_badges[badge_id]; + out.push(e('span', { + className: 'ffz-tooltip ffz-badge', + 'data-tooltip-type': 'badge', + 'data-provider': 'twitch', + 'data-badge': badge_id, + 'data-version': version + })); + } + + /*for(const badge of badges) + if ( badge && badge.id ) { + const full_badge = this.badges[badge.id], + style = {}, + props = { + className: 'ffz-tooltip ffz-badge', + 'data-tooltip-type': 'badge', + 'data-provider': 'ffz', + 'data-badge': badge.id, + style + }; + + if ( full_badge.image ) + style.backgroundImage = `url("${full_badge.image}")`; + + if ( full_badge.color ) + style.backgroundColor = full_badge.color; + + out.push(e('span', props)); + }*/ + + return out; + } + + + rebuildAllCSS() { + for(const room of this.parent.iterateRooms()) + room.buildBadgeCSS(); + + this.buildTwitchBadgeCSS(); + this.buildTwitchCSSBadgeCSS(); + } + + + // ======================================================================== + // Extension Badges + // ======================================================================== + + getBadges(user_id, user_login, room_id, room_login) { + const room = this.parent.getRoom(room_id, room_login, true), + global_user = this.parent.getUser(user_id, user_login, true), + room_user = room && room.getUser(user_id, user_login, true); + + return (global_user ? global_user.badges._cache : []).concat( + room_user ? room_user.badges._cache : []); + } + + + async loadGlobalBadges(tries = 0) { + let response, data; + try { + response = await fetch(`${API_SERVER}/v1/badges`); + } catch(err) { + tries++; + if ( tries < 10 ) + return setTimeout(() => this.loadGlobalBadges(tries), 500 * tries); + + this.log.error('Error loading global badge data.', err); + return false; + } + + if ( ! response.ok ) + return false; + + try { + data = await response.json(); + } catch(err) { + this.log.error('Error parsing global badge data.', err); + return false; + } + + let badges = 0, users = 0; + + if ( data.badges ) + for(const badge of data.badges) + if ( badge && badge.id ) { + this.loadBadgeData(badge.id, badge); + badges++; + } + + if ( data.users ) + for(const badge_id in data.users) + if ( has(data.users, badge_id) ) { + const badge = this.badges[badge_id]; + let c = 0; + for(const user_login of data.users[badge_id]) { + const user = this.parent.getUser(undefined, user_login); + if ( user.addBadge('ffz-global', badge_id) ) { + c++; + users++; + } + } + + if ( c > 0 ) + this.log.info(`Added "${badge ? badge.name : `#${badge_id}`}" to ${c} users.`); + } + + this.log.info(`Loaded ${badges} badges and assigned them to ${users} users.`); + } + + + loadBadgeData(badge_id, data) { + this.badges[badge_id] = data; + + if ( data.replaces && ! data.replaces_type ) { + data.replaces_type = data.replaces; + data.replaces = true; + } + + if ( data.name === 'developer' || data.name === 'supporter' ) + data.click_url = 'https://www.frankerfacez.com/donate'; + } + + + // ======================================================================== + // Twitch Badges + // ======================================================================== + + getTwitchBadge(badge, version, room_id, room_login) { + const room = this.parent.getRoom(room_id, room_login, true); + let b; + + if ( room ) { + const versions = room.badges.get(badge); + b = versions && versions.get(version); + } + + if ( ! b ) { + const versions = this.twitch_badges.get(badge); + b = version && versions.get(version); + } + + return b; + } + + updateTwitchBadges(badges) { + this.twitch_badges = badges; + this.buildTwitchBadgeCSS(); + } + + + buildTwitchCSSBadgeCSS() { + const style = this.parent.context.get('chat.badges.style'), + is_dark = this.parent.context.get('theme.is-dark'); + + const out = []; + for(const key in CSS_BADGES) + if ( has(CSS_BADGES, key) ) { + const data = CSS_BADGES[key]; + for(const version in data) + if ( has(data, version) ) { + const d = data[version], + selector = `.ffz-badge[data-badge="${key}"][data-version="${version}"]`; + + out.push(`${selector}{${generateBadgeCSS(key, version, d, style, is_dark)}}`); + } + } + + this.style.set('css-badges', out.join('\n')); + } + + + buildTwitchBadgeCSS() { + if ( ! this.twitch_badges ) + this.style.delete('twitch-badges'); + + const out = []; + for(const [key, versions] of this.twitch_badges) { + if ( has(CSS_BADGES, key) ) + continue; + + for(const [version, data] of versions) { + out.push(`.ffz-badge[data-badge="${key}"][data-version="${version}"] { + background-color: transparent; + filter: none; + ${WEBKIT}mask-image: none; + background-image: url("${data.image1x}"); + background-image: ${WEBKIT}image-set( + url("${data.image1x}") 1x, + url("${data.image2x}") 2x, + url("${data.image4x}") 4x + ); +}`) + } + } + + if ( out.length ) + this.style.set('twitch-badges', out.join('\n')); + else + this.style.delete('twitch-badges'); + } } \ No newline at end of file diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 334a5452..911a0e18 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -4,9 +4,6 @@ // Chat // ============================================================================ -import {IS_WEBKIT} from 'utilities/constants'; -const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; - import Module from 'utilities/module'; import {createElement, ManagedStyle} from 'utilities/dom'; import {timeout, has} from 'utilities/object'; @@ -242,13 +239,13 @@ export default class Chat extends Module { this.context.on('changed:theme.is-dark', () => { for(const key in this.rooms) if ( this.rooms[key] ) - this.rooms[key].updateBitsCSS(); + this.rooms[key].buildBitsCSS(); }); this.context.on('changed:chat.bits.animated', () => { for(const key in this.rooms) if ( this.rooms[key] ) - this.rooms[key].updateBitsCSS(); + this.rooms[key].buildBitsCSS(); }); } @@ -260,53 +257,6 @@ export default class Chat extends Module { } - getBadge(badge, version, room) { - let b; - if ( this.room_ids[room] ) { - const versions = this.room_ids[room].badges.get(badge); - b = versions && versions.get(version); - } - - if ( ! b ) { - const versions = this.badges.get(badge); - b = versions && versions.get(version); - } - - return b; - } - - - - updateBadges(badges) { - this.badges = badges; - this.updateBadgeCSS(); - } - - - updateBadgeCSS() { - if ( ! this.badges ) - this.style.delete('badges'); - - const out = []; - for(const [key, versions] of this.badges) - for(const [version, data] of versions) { - out.push(`.ffz-badge.badge--${key}.version--${version} { - background-color: transparent; - filter: none; - ${WEBKIT}mask-image: none; - background-image: url("${data.image1x}"); - background-image: ${WEBKIT}image-set( - url("${data.image1x}") 1x, - url("${data.image2x}") 2x, - url("${data.image4x}") 4x - ); -}`) - } - - this.style.set('badges', out.join('\n')); - } - - getUser(id, login, no_create, no_login) { let user; if ( id && typeof id === 'number' ) @@ -404,6 +354,27 @@ export default class Chat extends Module { } + * iterateRooms() { + const visited = new Set; + + for(const id in this.room_ids) + if ( has(this.room_ids, id) ) { + const room = this.room_ids[id]; + if ( room ) { + visited.add(room); + yield room; + } + } + + for(const login in this.rooms) + if ( has(this.rooms, login) ) { + const room = this.rooms[login]; + if ( room && ! visited.has(room) ) + yield room; + } + } + + formatTime(time) { if (!( time instanceof Date )) time = new Date(time); @@ -467,25 +438,6 @@ export default class Chat extends Module { } - renderBadges(msg, e) { // eslint-disable-line class-methods-use-this - const out = [], - badges = msg.badges || {}; - - for(const key in badges) - if ( has(badges, key) ) { - const version = badges[key]; - out.push(e('span', { - className: `ffz-tooltip ffz-badge badge--${key} version--${version}`, - 'data-tooltip-type': 'badge', - 'data-badge': key, - 'data-version': version - })) - } - - return out; - } - - renderTokens(tokens, e) { if ( ! e ) e = createElement; diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index f7301685..91b8c487 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -6,13 +6,11 @@ import User from './user'; -import {API_SERVER, IS_WEBKIT} from 'utilities/constants'; +import {API_SERVER, WEBKIT_CSS as WEBKIT} from 'utilities/constants'; import {ManagedStyle} from 'utilities/dom'; import {has, SourcedSet} from 'utilities/object'; -const WEBKIT = IS_WEBKIT ? '-webkit-' : ''; - export default class Room { constructor(manager, id, login) { @@ -270,10 +268,10 @@ export default class Room { updateBadges(badges) { this.badges = badges; - this.updateBadgeCSS(); + this.buildBadgeCSS(); } - updateBadgeCSS() { + buildBadgeCSS() { if ( ! this.badges ) return this.style.delete('badges'); @@ -282,7 +280,7 @@ export default class Room { for(const [key, versions] of this.badges) { for(const [version, data] of versions) { - const rule = `.ffz-badge.badge--${key}.version--${version}`; + const rule = `.ffz-badge[data-badge="${key}"][data-version="${version}"]`; out.push(`[data-room-id="${id}"] ${rule} { background-color: transparent; @@ -309,17 +307,16 @@ export default class Room { updateBitsConfig(config) { this.bitsConfig = config; - this.updateBitsCSS(); + this.buildBitsCSS(); } - updateBitsCSS() { + buildBitsCSS() { if ( ! this.bitsConfig ) return this.style.delete('bits'); const animated = this.manager.context.get('chat.bits.animated') ? 'animated' : 'static', - is_dark = this.manager.context.get('theme.is-dark'), - theme = is_dark ? 'DARK' : 'LIGHT', - antitheme = is_dark ? 'LIGHT' : 'DARK', + theme = this.manager.context.get('theme.is-dark') ? 'DARK' : 'LIGHT', + tt_theme = this.manager.context.get('theme.tooltips-dark') ? 'DARK' : 'LIGHT', out = []; for(const key in this.bitsConfig) @@ -331,7 +328,7 @@ export default class Room { for(let i=0; i < l; i++) { const images = tiers[i].images[theme][animated], - anti_images = tiers[i].images[antitheme][animated]; + tt_images = tiers[i].images[tt_theme][animated]; out.push(`.ffz-cheer[data-prefix="${prefix}"][data-tier="${i}"] { color: ${tiers[i].color}; @@ -343,15 +340,15 @@ export default class Room { ); } .ffz__tooltip .ffz-cheer[data-prefix="${prefix}"][data-tier="${i}"] { - background-image: url("${anti_images[1]}"); + background-image: url("${tt_images[1]}"); background-image: ${WEBKIT}image-set( - url("${anti_images[1]}") 1x, - url("${anti_images[2]}") 2x, - url("${anti_images[4]}") 4x + url("${tt_images[1]}") 1x, + url("${tt_images[2]}") 2x, + url("${tt_images[4]}") 4x ); } .ffz-cheer-preview[data-prefix="${prefix}"][data-tier="${i}"] { - background-image: url("${anti_images[4]}"); + background-image: url("${tt_images[4]}"); }`) } } diff --git a/src/modules/chat/user.js b/src/modules/chat/user.js index 6d3d45b3..60a08ebe 100644 --- a/src/modules/chat/user.js +++ b/src/modules/chat/user.js @@ -18,6 +18,7 @@ export default class User { (room || manager).user_ids[id] = this; this.emote_sets = new SourcedSet; + this.badges = new SourcedSet; } destroy() { @@ -59,18 +60,60 @@ export default class User { } + // ======================================================================== + // Add Badges + // ======================================================================== + + addBadge(provider, badge_id, data) { + if ( data ) + data.id = badge_id; + else + data = {id: badge_id}; + + if ( this.badges.has(provider) ) + for(const old_b of this.badges.get(provider)) + if ( old_b.id == badge_id ) { + Object.assign(old_b, data); + return false; + } + + this.badges.push(provider, data); + //this.manager.badges.refBadge(badge_id); + return true; + } + + + getBadge(badge_id) { + for(const badge of this.badges._cache) + if ( badge.id == badge_id ) + return badge; + } + + + removeBadge(provider, badge_id) { + if ( ! this.badges.has(provider) ) + return false; + + for(const old_b of this.badges.get(provider)) + if ( old_b.id == badge_id ) { + this.badges.remove(provider, old_b); + //this.manager.badges.unrefBadge(badge_id); + return true; + } + } + + + // ======================================================================== // Emote Sets // ======================================================================== - addSet(provider, set_id, data) { + addSet(provider, set_id) { if ( ! this.emote_sets.sourceIncludes(provider, set_id) ) { this.emote_sets.push(provider, set_id); this.manager.emotes.refSet(set_id); + return true; } - - if ( data ) - this.manager.emotes.loadSetData(set_id, data); } removeSet(provider, set_id) { diff --git a/src/modules/tooltips.js b/src/modules/tooltips.js index e4c72032..9e8d2a8f 100644 --- a/src/modules/tooltips.js +++ b/src/modules/tooltips.js @@ -17,9 +17,6 @@ export default class TooltipProvider extends Module { this.should_enable = true; - this.inject('i18n'); - this.inject('chat'); - this.types.json = target => { const title = target.dataset.title; return [ @@ -33,34 +30,6 @@ export default class TooltipProvider extends Module { }, target.dataset.data) ] } - - this.types.badge = (target, tip) => { - const container = target.parentElement.parentElement, - - badge = target.dataset.badge, - version = target.dataset.version, - room = container.dataset.roomId, - - data = this.chat.getBadge(badge, version, room); - - if ( ! data ) - return; - - return [ - this.chat.context.get('tooltip.badge-images') && e('img', { - className: 'preview-image', - src: data.image4x, - - style: { - height: '72px' - }, - - onLoad: tip.update - }), - - data.title - ] - }; } onEnable() { diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 3f138bf9..32e29af5 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -443,16 +443,15 @@ export default class ChatHook extends Module { if ( u ) e.emotes = u.emotes; - if ( original.action ) { + if ( original.action ) e.message = original.action; - - // Twitch doesn't generate a proper emote tag for echoed back - // actions, so we have to regenerate it. Fun. :D - if ( u && u.username === i.userLogin ) - e.emotes = findEmotes(e.message, i.selfEmotes); - - } else + else e.message = original.message.body; + + // Twitch doesn't generate a proper emote tag for echoed back + // actions, so we have to regenerate it. Fun. :D + if ( u && u.username === i.userLogin ) + e.emotes = findEmotes(e.message, i.selfEmotes); } //e.original = original; @@ -610,7 +609,7 @@ export default class ChatHook extends Module { return; if ( props.badgeSets ) { - this.chat.updateBadges(props.badgeSets.globalsBySet); + this.chat.badges.updateTwitchBadges(props.badgeSets.globalsBySet); this.updateRoomBadges(cont, props.badgeSets.channelsBySet); } } @@ -637,7 +636,7 @@ export default class ChatHook extends Module { obscl = obs.channelsBySet && obs.channelsBySet.size || 0; if ( bsgl !== obsgl ) - this.chat.updateBadges(bs.globalsBySet); + this.chat.badges.updateTwitchBadges(bs.globalsBySet); if ( bscl !== obscl ) this.updateRoomBadges(cont, bs.channelsBySet); diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 5f80a1ec..d7051f02 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -75,34 +75,33 @@ export default class ChatLine extends Module { if ( ! msg.message && msg.messageParts ) detokenizeMessage(msg); - const tokens = t.chat.tokenizeMessage(msg, {login: this.props.currentUserLogin, display: this.props.currentUserDisplayName}), - fragment = t.chat.renderTokens(tokens, e); + const tokens = msg.ffzTokens = msg.ffzTokens || t.chat.tokenizeMessage(msg, {login: this.props.currentUserLogin, display: this.props.currentUserDisplayName}); let cls = 'chat-line__message', - out = fragment.length ? [ + out = tokens.length ? [ this.props.showTimestamps && e('span', { className: 'chat-line__timestamp' }, t.chat.formatTime(msg.timestamp)), this.renderModerationIcons(), e('span', { className: 'chat-line__message--badges' - }, t.chat.renderBadges(msg, e)), + }, t.chat.badges.render(msg, e)), e('a', { - className: 'chat-author__display-name', + className: 'chat-author__display-name notranslate', style: { color }, onClick: this.usernameClickHandler - }, user.userDisplayName), - user.isIntl && e('span', { - className: 'chat-author__intl-login', - style: { color }, - onClick: this.usernameClickHandler - }, ` (${user.userLogin})`), + }, [ + user.userDisplayName, + user.isIntl && e('span', { + className: 'chat-author__intl-login' + }, ` (${user.userLogin})`) + ]), e('span', null, is_action ? ' ' : ': '), show ? e('span', { className:'message', style: is_action ? { color } : null - }, fragment) + }, t.chat.renderTokens(tokens, e)) : e('span', { className: 'chat-line__message--deleted', diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index 9c429728..ca77759d 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -45,7 +45,7 @@ export default class Scroller extends Module { } onEnable() { - this.i18n.on('i18n:update', () => { + this.on('i18n:update', () => { for(const inst of this.ChatScroller.instances) inst.ffzUpdateText(); }); diff --git a/src/sites/twitch-twilight/modules/theme/index.js b/src/sites/twitch-twilight/modules/theme/index.js index b5d074d6..d27f21af 100644 --- a/src/sites/twitch-twilight/modules/theme/index.js +++ b/src/sites/twitch-twilight/modules/theme/index.js @@ -42,6 +42,13 @@ export default class ThemeEngine extends Module { } }); + this.settings.add('theme.tooltips-dark', { + requires: ['theme.is-dark'], + process(ctx) { + return ! ctx.get('theme.is-dark') + } + }); + this._style = null; } diff --git a/src/utilities/constants.js b/src/utilities/constants.js index bc99c32d..23f09b05 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -21,4 +21,6 @@ export const WS_CLUSTERS = { export const IS_OSX = navigator.platform ? navigator.platform.indexOf('Mac') !== -1 : /OS X/.test(navigator.userAgent); export const IS_WIN = navigator.platform ? navigator.platform.indexOf('Win') !== -1 : /Windows/.test(navigator.userAgent); -export const IS_WEBKIT = navigator.userAgent.indexOf('AppleWebKit/') !== -1 && navigator.userAgent.indexOf('Edge/') === -1; \ No newline at end of file +export const IS_WEBKIT = navigator.userAgent.indexOf('AppleWebKit/') !== -1 && navigator.userAgent.indexOf('Edge/') === -1; + +export const WEBKIT_CSS = IS_WEBKIT ? '-webkit-' : ''; \ No newline at end of file diff --git a/src/utilities/object.js b/src/utilities/object.js index e1b4dab4..791928b2 100644 --- a/src/utilities/object.js +++ b/src/utilities/object.js @@ -170,6 +170,9 @@ export function maybe_call(fn, ctx, ...args) { const SPLIT_REGEX = /[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g; export function split_chars(str) { + if ( str === '' ) + return []; + return str.match(SPLIT_REGEX); } diff --git a/styles/tooltips.scss b/styles/tooltips.scss index 36db1f11..396609b3 100644 --- a/styles/tooltips.scss +++ b/styles/tooltips.scss @@ -80,6 +80,12 @@ body { text-align: center; .preview-image { + &.ffz-badge { + height: 72px; + width: 72px; + background-repeat: no-repeat; + } + display: block; margin: 3px auto 6px; }