2017-11-13 01:23:39 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Default Tokenizers
|
|
|
|
// ============================================================================
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
import {sanitize, createElement} from 'utilities/dom';
|
2017-11-23 02:49:23 -05:00
|
|
|
import {has, split_chars} from 'utilities/object';
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-09 19:57:05 -04:00
|
|
|
import {TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants';
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
const EMOTE_CLASS = 'chat-line__message--emote',
|
2018-04-02 03:30:22 -04:00
|
|
|
LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g,
|
2018-04-11 20:48:02 -04:00
|
|
|
MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Links
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
const TOOLTIP_VERSION = 4;
|
|
|
|
|
|
|
|
export const Links = {
|
|
|
|
type: 'link',
|
|
|
|
priority: 50,
|
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-link.vue'),
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
render(token, createElement) {
|
|
|
|
return (<a
|
|
|
|
class="ffz-tooltip"
|
|
|
|
data-tooltip-type="link"
|
|
|
|
data-url={token.url}
|
|
|
|
data-is-mail={token.is_mail}
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
target="_blank"
|
|
|
|
href={token.url}
|
|
|
|
>{token.text}</a>);
|
2017-11-13 01:23:39 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
tooltip(target, tip) {
|
|
|
|
if ( ! this.context.get('tooltip.rich-links') )
|
|
|
|
return '';
|
|
|
|
|
|
|
|
if ( target.dataset.isMail === 'true' )
|
|
|
|
return [this.i18n.t('tooltip.email-link', 'E-Mail %{address}', {address: target.textContent})];
|
|
|
|
|
|
|
|
return this.get_link_info(target.dataset.url).then(data => {
|
|
|
|
if ( ! data || (data.v || 1) > TOOLTIP_VERSION )
|
|
|
|
return '';
|
|
|
|
|
|
|
|
let content = data.content || data.html || '';
|
|
|
|
|
|
|
|
// TODO: Replace timestamps.
|
|
|
|
|
|
|
|
if ( data.urls && data.urls.length > 1 )
|
|
|
|
content += (content.length ? '<hr>' : '') +
|
|
|
|
sanitize(this.i18n.t(
|
|
|
|
'tooltip.link-destination',
|
|
|
|
'Destination: %{url}',
|
2017-12-13 17:35:20 -05:00
|
|
|
{url: data.urls[data.urls.length-1][1]}
|
2017-11-13 01:23:39 -05:00
|
|
|
));
|
|
|
|
|
|
|
|
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 ? `<hr>${content}` : '');
|
|
|
|
}
|
|
|
|
|
|
|
|
const show_image = this.context.get('tooltip.link-images') && (data.image_safe || this.context.get('tooltip.link-nsfw-images'));
|
|
|
|
|
|
|
|
if ( show_image ) {
|
|
|
|
if ( data.image && ! data.image_iframe )
|
|
|
|
content = `<img class="preview-image" src="${sanitize(data.image)}">${content}`
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if ( tip.element )
|
|
|
|
for(const el of tip.element.querySelectorAll('video,img'))
|
|
|
|
el.addEventListener('load', tip.update)
|
|
|
|
});
|
|
|
|
|
|
|
|
} else if ( content.length )
|
|
|
|
content = content.replace(/<!--MS-->.*<!--ME-->/g, '');
|
|
|
|
|
|
|
|
if ( data.tooltip_class )
|
|
|
|
tip.element.classList.add(data.tooltip_class);
|
|
|
|
|
|
|
|
return content;
|
|
|
|
|
|
|
|
}).catch(error =>
|
|
|
|
sanitize(this.i18n.t('tooltip.error', 'An error occurred. (%{error})', {error}))
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
2018-04-02 03:30:22 -04:00
|
|
|
process(tokens) {
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( ! tokens || ! tokens.length )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
const out = [];
|
|
|
|
for(const token of tokens) {
|
|
|
|
if ( token.type !== 'text' ) {
|
|
|
|
out.push(token);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
LINK_REGEX.lastIndex = 0;
|
|
|
|
const text = token.text;
|
|
|
|
let idx = 0, match;
|
|
|
|
|
|
|
|
while((match = LINK_REGEX.exec(text))) {
|
|
|
|
const nix = match.index + (match[1] ? match[1].length : 0);
|
|
|
|
if ( idx !== nix )
|
|
|
|
out.push({type: 'text', text: text.slice(idx, nix)});
|
|
|
|
|
|
|
|
const is_mail = ! match[3] && match[2].indexOf('/') === -1 && match[2].indexOf('@') !== -1;
|
|
|
|
|
|
|
|
out.push({
|
|
|
|
type: 'link',
|
|
|
|
url: (match[3] ? '' : is_mail ? 'mailto:' : 'https://') + match[2],
|
|
|
|
is_mail,
|
|
|
|
text: match[2]
|
|
|
|
});
|
|
|
|
|
|
|
|
idx = nix + match[2].length;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( idx < text.length )
|
|
|
|
out.push({type: 'text', text: text.slice(idx)});
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-02 03:30:22 -04:00
|
|
|
Links.tooltip.interactive = function(target) {
|
2017-11-14 04:11:43 -05:00
|
|
|
if ( ! this.context.get('tooltip.rich-links') || ! this.context.get('tooltip.link-interaction') || target.dataset.isMail === 'true' )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const info = this.get_link_info(target.dataset.url, true);
|
|
|
|
return info && info.interactive;
|
|
|
|
};
|
|
|
|
|
2018-04-02 03:30:22 -04:00
|
|
|
Links.tooltip.delayHide = function(target) {
|
2017-11-14 04:11:43 -05:00
|
|
|
if ( ! this.context.get('tooltip.rich-links') || ! this.context.get('tooltip.link-interaction') || target.dataset.isMail === 'true' )
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return 64;
|
|
|
|
};
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Rich Content
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
/*export const RichContent = {
|
|
|
|
type: 'rich-content',
|
|
|
|
|
|
|
|
render(token, e) {
|
|
|
|
return e('div', {
|
|
|
|
className: 'ffz--rich-content elevation-1 mg-y-05',
|
|
|
|
}, e('a', {
|
|
|
|
className: 'clips-chat-card flex flex-nowrap pd-05',
|
|
|
|
target: '_blank',
|
|
|
|
href: token.url
|
|
|
|
}, [
|
|
|
|
e('div', {
|
|
|
|
className: 'clips-chat-card__thumb align-items-center flex justify-content-center'
|
|
|
|
})
|
|
|
|
]));
|
|
|
|
},
|
|
|
|
|
|
|
|
process(tokens, msg) {
|
|
|
|
if ( ! tokens || ! tokens.length )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
for(const token of tokens) {
|
|
|
|
if ( token.type !== 'link' )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Mentions
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export const Mentions = {
|
|
|
|
type: 'mention',
|
2018-04-07 17:59:16 -04:00
|
|
|
priority: 0,
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-mention.vue'),
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
render(token, createElement) {
|
|
|
|
return (<strong class={`chat-line__message-mention${token.me ? ' ffz--mention-me' : ''}`}>
|
|
|
|
{token.text}
|
|
|
|
</strong>);
|
2017-11-13 01:23:39 -05:00
|
|
|
},
|
|
|
|
|
2017-11-17 14:59:46 -05:00
|
|
|
process(tokens, msg, user) {
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( ! tokens || ! tokens.length )
|
|
|
|
return tokens;
|
|
|
|
|
2017-11-17 14:59:46 -05:00
|
|
|
let regex, login, display;
|
|
|
|
if ( user && user.login ) {
|
|
|
|
login = user.login.toLowerCase();
|
2018-04-28 17:56:03 -04:00
|
|
|
display = user.displayName && user.displayName.toLowerCase();
|
2017-11-17 14:59:46 -05:00
|
|
|
if ( display === login )
|
|
|
|
display = null;
|
|
|
|
|
|
|
|
regex = new RegExp(`([^\\w@#%\\-+=:~]|\\b)?(@?(${user.login.toLowerCase()}${display ? `|${display}` : ''})|@([^\\u0000-\\u007F]+|\\w+)+)([^\\w.\\/@#%&()\\-+=:?~]|\\s|\\b|$)`, 'gi');
|
|
|
|
} else
|
|
|
|
regex = MENTION_REGEX;
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
const out = [];
|
|
|
|
for(const token of tokens) {
|
|
|
|
if ( token.type !== 'text' ) {
|
|
|
|
out.push(token);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-11-17 14:59:46 -05:00
|
|
|
regex.lastIndex = 0;
|
2017-11-13 01:23:39 -05:00
|
|
|
const text = token.text;
|
|
|
|
let idx = 0, match;
|
|
|
|
|
2017-11-17 14:59:46 -05:00
|
|
|
while((match = regex.exec(text))) {
|
|
|
|
const nix = match.index + (match[1] ? match[1].length : 0),
|
|
|
|
m = match[3] || match[4],
|
|
|
|
ml = m.toLowerCase(),
|
|
|
|
me = ml === login || ml === display;
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( idx !== nix )
|
|
|
|
out.push({type: 'text', text: text.slice(idx, nix)});
|
|
|
|
|
2017-11-17 14:59:46 -05:00
|
|
|
if ( me )
|
|
|
|
msg.mentioned = true;
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
out.push({
|
|
|
|
type: 'mention',
|
2017-11-17 14:59:46 -05:00
|
|
|
text: match[2],
|
|
|
|
me,
|
|
|
|
recipient: m
|
2017-11-13 01:23:39 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
idx = nix + match[2].length;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( idx < text.length )
|
|
|
|
out.push({type: 'text', text: text.slice(idx)});
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Cheers
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export const CheerEmotes = {
|
|
|
|
type: 'cheer',
|
2018-04-07 17:59:16 -04:00
|
|
|
priority: 40,
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-cheer.vue'),
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
render(token, createElement) {
|
|
|
|
return (<span
|
|
|
|
class="ffz-cheer ffz-tooltip"
|
|
|
|
data-tooltip-type="cheer"
|
|
|
|
data-prefix={token.prefix}
|
|
|
|
data-amount={this.i18n.formatNumber(token.amount)}
|
|
|
|
data-tier={token.tier}
|
|
|
|
data-individuals={JSON.stringify(token.individuals || null)}
|
|
|
|
alt={token.text}
|
|
|
|
/>);
|
2017-11-13 01:23:39 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
tooltip(target) {
|
|
|
|
const ds = target.dataset,
|
|
|
|
amount = parseInt(ds.amount.replace(/,/g, ''), 10),
|
|
|
|
prefix = ds.prefix,
|
|
|
|
tier = ds.tier,
|
|
|
|
individuals = ds.individuals && JSON.parse(ds.individuals),
|
|
|
|
length = individuals && individuals.length;
|
|
|
|
|
|
|
|
const out = [
|
2018-04-01 18:24:08 -04:00
|
|
|
this.context.get('tooltip.emote-images') && (<div
|
|
|
|
class="preview-image ffz-cheer-preview"
|
|
|
|
data-prefix={prefix}
|
|
|
|
data-tier={tier}
|
|
|
|
/>),
|
2017-11-13 01:23:39 -05:00
|
|
|
this.i18n.t('tooltip.bits', '%{count|number} Bits', amount),
|
|
|
|
];
|
|
|
|
|
|
|
|
if ( length > 1 ) {
|
2018-04-01 18:24:08 -04:00
|
|
|
out.push(<br />);
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
individuals.sort(i => -i[0]);
|
|
|
|
|
|
|
|
for(let i=0; i < length && i < 12; i++) {
|
|
|
|
const [amount, tier, prefix] = individuals[i];
|
|
|
|
out.push(this.tokenizers.cheer.render.call(this, {
|
|
|
|
amount,
|
|
|
|
prefix,
|
|
|
|
tier
|
2018-04-01 18:24:08 -04:00
|
|
|
}, createElement));
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( length > 12 ) {
|
2018-04-01 18:24:08 -04:00
|
|
|
out.push(<br />);
|
2017-11-13 01:23:39 -05:00
|
|
|
out.push(this.i18n.t('tooltip.bits.more', '(and %{count} more)', length-12));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
},
|
|
|
|
|
|
|
|
process(tokens, msg) {
|
|
|
|
if ( ! tokens || ! tokens.length || ! msg.bits )
|
|
|
|
return tokens;
|
|
|
|
|
2017-11-14 22:11:58 -05:00
|
|
|
// TODO: Store the room onto the chat message so we don't need to look this up.
|
2017-11-13 01:23:39 -05:00
|
|
|
const SiteChat = this.resolve('site.chat'),
|
|
|
|
chat = SiteChat && SiteChat.currentChat,
|
|
|
|
bitsConfig = chat && chat.props.bitsConfig;
|
|
|
|
|
|
|
|
if ( ! bitsConfig )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
const actions = bitsConfig.indexedActions,
|
2017-12-01 15:33:06 -05:00
|
|
|
matcher = new RegExp(`^(${Object.keys(actions).join('|')})(\\d+)$`, 'i');
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
const out = [],
|
|
|
|
collected = {},
|
|
|
|
collect = this.context.get('chat.bits.stack');
|
|
|
|
|
|
|
|
for(const token of tokens) {
|
2017-12-01 15:33:06 -05:00
|
|
|
if ( ! token || token.type !== 'text' ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
out.push(token);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
let text = [];
|
|
|
|
for(const segment of token.text.split(/ +/)) {
|
|
|
|
const match = matcher.exec(segment);
|
|
|
|
if ( match ) {
|
|
|
|
const prefix = match[1].toLowerCase(),
|
|
|
|
cheer = actions[prefix];
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
if ( ! cheer ) {
|
|
|
|
text.push(segment);
|
|
|
|
continue;
|
|
|
|
}
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
const amount = parseInt(match[2], 10),
|
|
|
|
tiers = cheer.orderedTiers;
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
let tier, token;
|
|
|
|
for(let i=0, l = tiers.length; i < l; i++)
|
|
|
|
if ( amount >= tiers[i].bits ) {
|
|
|
|
tier = i;
|
|
|
|
break;
|
|
|
|
}
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
if ( text.length ) {
|
|
|
|
// We have pending text. Join it together, with an extra space.
|
|
|
|
out.push({type: 'text', text: `${text.join(' ')} `});
|
|
|
|
text = [];
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
out.push(token = {
|
|
|
|
type: 'cheer',
|
|
|
|
prefix,
|
|
|
|
tier,
|
|
|
|
amount,
|
|
|
|
text: match[0]
|
|
|
|
});
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
if ( collect ) {
|
2018-03-25 22:16:16 -04:00
|
|
|
let pref = collect === 2 ? 'cheer' : prefix;
|
|
|
|
if ( ! actions[pref] )
|
|
|
|
pref = prefix;
|
|
|
|
|
|
|
|
const group = collected[pref] = collected[pref] || {total: 0, individuals: []};
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
group.total += amount;
|
|
|
|
group.individuals.push([amount, tier, prefix]);
|
|
|
|
token.hidden = true;
|
|
|
|
}
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-15 17:19:22 -04:00
|
|
|
text.push('');
|
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
} else
|
|
|
|
text.push(segment);
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
2017-12-01 15:33:06 -05:00
|
|
|
if ( text.length > 1 || (text.length === 1 && text[0] !== '') )
|
|
|
|
out.push({type: 'text', text: text.join(' ')});
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( collect ) {
|
|
|
|
for(const prefix in collected)
|
|
|
|
if ( has(collected, prefix) ) {
|
|
|
|
const cheers = collected[prefix],
|
|
|
|
cheer = actions[prefix],
|
|
|
|
tiers = cheer.orderedTiers;
|
|
|
|
|
|
|
|
let tier = 0;
|
|
|
|
for(let l = tiers.length; tier < l; tier++)
|
|
|
|
if ( cheers.total >= tiers[tier].bits )
|
|
|
|
break;
|
|
|
|
|
|
|
|
out.unshift({
|
|
|
|
type: 'cheer',
|
|
|
|
prefix,
|
|
|
|
tier,
|
|
|
|
amount: cheers.total,
|
|
|
|
individuals: cheers.individuals,
|
|
|
|
length: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Addon Emotes
|
|
|
|
// ============================================================================
|
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
const render_emote = (token, createElement) => {
|
|
|
|
const mods = token.modifiers || [], ml = mods.length,
|
|
|
|
emote = createElement('img', {
|
|
|
|
class: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`,
|
|
|
|
attrs: {
|
|
|
|
src: token.src,
|
|
|
|
srcSet: token.srcSet,
|
|
|
|
alt: token.text,
|
|
|
|
'data-tooltip-type': 'emote',
|
|
|
|
'data-provider': token.provider,
|
|
|
|
'data-id': token.id,
|
|
|
|
'data-set': token.set,
|
|
|
|
'data-code': token.code,
|
|
|
|
'data-variant': token.variant,
|
|
|
|
'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null,
|
|
|
|
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if ( ! ml )
|
|
|
|
return emote;
|
|
|
|
|
|
|
|
return createElement('span', {
|
|
|
|
class: `${EMOTE_CLASS} modified-emote`,
|
|
|
|
attrs: {
|
|
|
|
'data-provider': token.provider,
|
|
|
|
'data-id': token.id,
|
|
|
|
'data-set': token.set
|
|
|
|
}
|
|
|
|
}, [emote, mods.map(x => createElement('span', {key: x.text}, render_emote(x, createElement)))])
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
export const AddonEmotes = {
|
|
|
|
type: 'emote',
|
2018-04-07 17:59:16 -04:00
|
|
|
priority: 10,
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-05-10 19:56:39 -04:00
|
|
|
component: {
|
|
|
|
functional: true,
|
|
|
|
render(createElement, {props}) {
|
|
|
|
return render_emote(props.token, createElement);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
render(token, createElement) {
|
2017-11-13 01:23:39 -05:00
|
|
|
const mods = token.modifiers || [], ml = mods.length,
|
2018-04-01 18:24:08 -04:00
|
|
|
emote = (<img
|
2018-04-12 20:30:00 -04:00
|
|
|
class={`${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`}
|
2018-04-01 18:24:08 -04:00
|
|
|
src={token.src}
|
|
|
|
srcSet={token.srcSet}
|
|
|
|
alt={token.text}
|
|
|
|
data-tooltip-type="emote"
|
|
|
|
data-provider={token.provider}
|
|
|
|
data-id={token.id}
|
|
|
|
data-set={token.set}
|
2018-04-12 20:30:00 -04:00
|
|
|
data-code={token.code}
|
|
|
|
data-variant={token.variant}
|
2018-04-01 18:24:08 -04:00
|
|
|
data-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
|
|
|
|
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
|
2018-04-09 19:57:05 -04:00
|
|
|
onClick={this.emotes.handleClick}
|
2018-04-01 18:24:08 -04:00
|
|
|
/>);
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
if ( ! ml )
|
|
|
|
return emote;
|
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
return (<span
|
|
|
|
class={`${EMOTE_CLASS} modified-emote`}
|
|
|
|
data-provider={token.provider}
|
|
|
|
data-id={token.id}
|
|
|
|
data-set={token.set}
|
2018-04-09 19:57:05 -04:00
|
|
|
onClick={this.emotes.handleClick}
|
2018-04-01 18:24:08 -04:00
|
|
|
>
|
|
|
|
{emote}
|
2018-04-12 20:30:00 -04:00
|
|
|
{mods.map(t => <span key={t.text}>{this.tokenizers.emote.render.call(this, t, createElement)}</span>)}
|
2018-04-01 18:24:08 -04:00
|
|
|
</span>);
|
2017-11-13 01:23:39 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
tooltip(target, tip) {
|
2018-04-06 21:12:12 -04:00
|
|
|
const ds = target.dataset,
|
|
|
|
provider = ds.provider,
|
|
|
|
modifiers = ds.modifierInfo;
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-12 20:30:00 -04:00
|
|
|
let name, preview, source, owner, mods, fav_source, emote_id,
|
|
|
|
plain_name = false,
|
|
|
|
hide_source = ds.noSource === 'true';
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
if ( modifiers && modifiers !== 'null' ) {
|
|
|
|
mods = JSON.parse(modifiers).map(([set_id, emote_id]) => {
|
|
|
|
const emote_set = this.emotes.emote_sets[set_id],
|
|
|
|
emote = emote_set && emote_set.emotes[emote_id];
|
|
|
|
|
|
|
|
if ( emote )
|
2018-04-01 18:24:08 -04:00
|
|
|
return (<span>
|
2018-04-12 20:30:00 -04:00
|
|
|
{this.tokenizers.emote.render.call(this, emote.token, createElement)}
|
2018-04-01 18:24:08 -04:00
|
|
|
{` - ${emote.hidden ? '???' : emote.name}`}
|
|
|
|
</span>);
|
2017-11-13 01:23:39 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( provider === 'twitch' ) {
|
2018-04-09 19:57:05 -04:00
|
|
|
emote_id = parseInt(ds.id, 10);
|
|
|
|
const set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
|
2017-11-16 15:54:58 -05:00
|
|
|
emote_set = set_id != null && this.emotes.getTwitchSetChannel(set_id, tip.rerender);
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
preview = `//static-cdn.jtvnw.net/emoticons/v1/${emote_id}/4.0?_=preview`;
|
2018-04-09 19:57:05 -04:00
|
|
|
fav_source = 'twitch';
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
if ( emote_set ) {
|
|
|
|
source = emote_set.c_name;
|
|
|
|
|
|
|
|
if ( source === '--global--' || emote_id === 80393 )
|
|
|
|
source = this.i18n.t('emote.global', 'Twitch Global');
|
|
|
|
|
|
|
|
else if ( source === '--twitch-turbo--' || source === 'turbo' || source === '--turbo-faces--' )
|
|
|
|
source = this.i18n.t('emote.turbo', 'Twitch Turbo');
|
|
|
|
|
|
|
|
else if ( source === '--prime--' || source === '--prime-faces--' )
|
|
|
|
source = this.i18n.t('emote.prime', 'Twitch Prime');
|
|
|
|
|
|
|
|
else
|
|
|
|
source = this.i18n.t('tooltip.channel', 'Channel: %{source}', {source});
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if ( provider === 'ffz' ) {
|
2018-04-06 21:12:12 -04:00
|
|
|
const emote_set = this.emotes.emote_sets[ds.set],
|
|
|
|
emote = emote_set && emote_set.emotes[ds.id];
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-09 19:57:05 -04:00
|
|
|
if ( emote_set ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
source = emote_set.source_line || (`${emote_set.source || 'FFZ'} ${emote_set.title || 'Global'}`);
|
2018-04-09 19:57:05 -04:00
|
|
|
fav_source = emote_set.source || 'ffz';
|
|
|
|
}
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
if ( emote ) {
|
2018-04-09 19:57:05 -04:00
|
|
|
emote_id = emote.id;
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( emote.owner )
|
|
|
|
owner = this.i18n.t(
|
|
|
|
'emote.owner', 'By: %{owner}',
|
|
|
|
{owner: emote.owner.display_name});
|
|
|
|
|
|
|
|
if ( emote.urls[4] )
|
|
|
|
preview = emote.urls[4];
|
|
|
|
else if ( emote.urls[2] )
|
|
|
|
preview = emote.urls[2];
|
|
|
|
}
|
2018-04-09 19:57:05 -04:00
|
|
|
|
2018-04-12 20:30:00 -04:00
|
|
|
} else if ( provider === 'emoji' ) {
|
|
|
|
const emoji = this.emoji.emoji[ds.code],
|
|
|
|
style = this.context.get('chat.emoji.style'),
|
|
|
|
variant = ds.variant ? emoji.variants[ds.variant] : emoji,
|
|
|
|
vcode = ds.variant ? this.emoji.emoji[ds.variant] : null;
|
|
|
|
|
|
|
|
fav_source = 'emoji';
|
|
|
|
emote_id = ds.code;
|
|
|
|
|
|
|
|
preview = (<img
|
|
|
|
class="preview-image ffz-emoji"
|
|
|
|
src={this.emoji.getFullImage(variant.image, style)}
|
|
|
|
srcSet={this.emoji.getFullImageSet(variant.image, style)}
|
|
|
|
onLoad={tip.update}
|
|
|
|
/>);
|
|
|
|
|
|
|
|
plain_name = true;
|
|
|
|
name = `:${emoji.names[0]}:${vcode ? `:${vcode.names[0]}:` : ''}`;
|
|
|
|
source = this.i18n.t('tooltip.emoji', 'Emoji - %{category}', emoji);
|
|
|
|
|
2018-04-09 19:57:05 -04:00
|
|
|
} else
|
|
|
|
return;
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-12 20:30:00 -04:00
|
|
|
if ( ! name )
|
|
|
|
name = ds.name || target.alt;
|
|
|
|
|
|
|
|
const favorite = fav_source && this.emotes.isFavorite(fav_source, emote_id);
|
2018-04-06 21:12:12 -04:00
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
return [
|
2018-04-12 20:30:00 -04:00
|
|
|
preview && this.context.get('tooltip.emote-images') && (typeof preview === 'string' ? (<img
|
2018-04-01 18:24:08 -04:00
|
|
|
class="preview-image"
|
|
|
|
src={preview}
|
|
|
|
onLoad={tip.update}
|
2018-04-12 20:30:00 -04:00
|
|
|
/>) : preview),
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-12 20:30:00 -04:00
|
|
|
plain_name || (hide_source && ! owner) ? name : this.i18n.t('tooltip.emote', 'Emote: %{name}', {name}),
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-06 21:12:12 -04:00
|
|
|
! hide_source && source && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
2018-04-01 18:24:08 -04:00
|
|
|
{source}
|
|
|
|
</div>),
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-01 18:24:08 -04:00
|
|
|
owner && this.context.get('tooltip.emote-sources') && (<div class="tw-pd-t-05">
|
|
|
|
{owner}
|
|
|
|
</div>),
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-04-06 21:12:12 -04:00
|
|
|
ds.sellout && (<div class="tw-mg-t-05 tw-border-t tw-pd-t-05">{ds.sellout}</div>),
|
|
|
|
|
2018-04-09 19:57:05 -04:00
|
|
|
mods && (<div class="tw-pd-t-1">{mods}</div>),
|
|
|
|
|
|
|
|
favorite && (<figure class="ffz--favorite ffz-i-star" />)
|
2017-11-13 01:23:39 -05:00
|
|
|
];
|
|
|
|
},
|
|
|
|
|
|
|
|
process(tokens, msg) {
|
|
|
|
if ( ! tokens || ! tokens.length )
|
|
|
|
return tokens;
|
|
|
|
|
2017-11-22 15:39:38 -05:00
|
|
|
const emotes = this.emotes.getEmotes(
|
2017-11-13 01:23:39 -05:00
|
|
|
msg.user.userID,
|
|
|
|
msg.user.userLogin,
|
|
|
|
msg.roomID,
|
|
|
|
msg.roomLogin
|
|
|
|
),
|
|
|
|
out = [];
|
|
|
|
|
2017-11-22 15:39:38 -05:00
|
|
|
if ( ! emotes )
|
2017-11-13 01:23:39 -05:00
|
|
|
return tokens;
|
|
|
|
|
|
|
|
let last_token, emote;
|
|
|
|
for(const token of tokens) {
|
|
|
|
if ( ! token )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if ( token.type !== 'text' ) {
|
|
|
|
if ( token.type === 'emote' && ! token.modifiers )
|
|
|
|
token.modifiers = [];
|
|
|
|
|
|
|
|
out.push(token);
|
|
|
|
last_token = token;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let text = [];
|
|
|
|
|
|
|
|
for(const segment of token.text.split(/ +/)) {
|
|
|
|
if ( has(emotes, segment) ) {
|
|
|
|
emote = emotes[segment];
|
|
|
|
|
|
|
|
// Is this emote a modifier?
|
|
|
|
if ( emote.modifier && last_token && last_token.modifiers && (!text.length || (text.length === 1 && text[0] === '')) ) {
|
|
|
|
if ( last_token.modifiers.indexOf(emote.token) === -1 )
|
|
|
|
last_token.modifiers.push(emote.token);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( text.length ) {
|
|
|
|
// We have pending text. Join it together, with an extra space.
|
2017-11-22 15:39:38 -05:00
|
|
|
const t = {type: 'text', text: `${text.join(' ')} `};
|
2017-11-13 01:23:39 -05:00
|
|
|
out.push(t);
|
|
|
|
if ( t.text.trim().length )
|
|
|
|
last_token = t;
|
|
|
|
|
|
|
|
text = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const t = Object.assign({modifiers: []}, emote.token);
|
|
|
|
out.push(t);
|
|
|
|
last_token = t;
|
|
|
|
|
|
|
|
text.push('');
|
|
|
|
|
|
|
|
} else
|
|
|
|
text.push(segment);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( text.length > 1 || (text.length === 1 && text[0] !== '') )
|
|
|
|
out.push({type: 'text', text: text.join(' ')});
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-04-12 20:30:00 -04:00
|
|
|
// ============================================================================
|
|
|
|
// Emoji
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export const Emoji = {
|
|
|
|
type: 'emoji',
|
|
|
|
priority: 15,
|
|
|
|
|
|
|
|
process(tokens) {
|
|
|
|
if ( ! tokens || ! tokens.length )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
const splitter = this.emoji.splitter,
|
|
|
|
style = this.context.get('chat.emoji.style'),
|
|
|
|
out = [];
|
|
|
|
|
|
|
|
if ( style === 0 )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
for(const token of tokens) {
|
|
|
|
if ( ! token )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if ( token.type !== 'text' ) {
|
|
|
|
out.push(token);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const text = token.text;
|
|
|
|
|
|
|
|
splitter.lastIndex = 0;
|
|
|
|
let idx = 0, match;
|
|
|
|
|
|
|
|
while((match = splitter.exec(text))) {
|
|
|
|
const start = match.index,
|
|
|
|
key = this.emoji.chars.get(match[0]);
|
|
|
|
|
|
|
|
if ( ! key )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
const emoji = this.emoji.emoji[key[0]],
|
|
|
|
variant = key[1] ? emoji.variants[key[1]] : emoji,
|
|
|
|
length = split_chars(match[0]).length;
|
|
|
|
|
|
|
|
if ( idx !== start )
|
|
|
|
out.push({type: 'text', text: text.slice(idx, start)});
|
|
|
|
|
|
|
|
out.push({
|
|
|
|
type: 'emote',
|
|
|
|
|
|
|
|
provider: 'emoji',
|
|
|
|
code: key[0],
|
|
|
|
variant: key[1],
|
|
|
|
|
|
|
|
src: this.emoji.getFullImage(variant.image, style),
|
|
|
|
srcSet: this.emoji.getFullImageSet(variant.image, style),
|
|
|
|
|
|
|
|
text: match[0],
|
|
|
|
length,
|
|
|
|
modifiers: []
|
|
|
|
});
|
|
|
|
|
|
|
|
idx = start + match[0].length;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( idx < text.length )
|
|
|
|
out.push({type: 'text', text: text.slice(idx)});
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
// ============================================================================
|
|
|
|
// Twitch Emotes
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export const TwitchEmotes = {
|
|
|
|
type: 'twitch-emote',
|
2018-04-07 17:59:16 -04:00
|
|
|
priority: 20,
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
process(tokens, msg) {
|
|
|
|
if ( ! msg.emotes )
|
|
|
|
return tokens;
|
|
|
|
|
|
|
|
const data = msg.emotes,
|
|
|
|
emotes = [];
|
|
|
|
|
|
|
|
for(const emote_id in data)
|
|
|
|
if ( has(data, emote_id) ) {
|
|
|
|
for(const match of data[emote_id])
|
|
|
|
emotes.push([emote_id, match.startIndex, match.endIndex + 1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
const out = [],
|
|
|
|
e_length = emotes.length;
|
|
|
|
|
|
|
|
if ( ! e_length )
|
|
|
|
return tokens;
|
|
|
|
|
2017-11-14 22:11:58 -05:00
|
|
|
emotes.sort((a,b) => a[1] !== b[1] ? a[1] - b[1] : b[0] - a[0]);
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
let idx = 0,
|
|
|
|
eix = 0;
|
|
|
|
|
|
|
|
for(const token of tokens) {
|
|
|
|
const length = token.length || (token.text && split_chars(token.text).length) || 0,
|
|
|
|
t_start = idx,
|
|
|
|
t_end = idx + length;
|
|
|
|
|
|
|
|
if ( token.type !== 'text' ) {
|
|
|
|
out.push(token);
|
|
|
|
idx = t_end;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const text = split_chars(token.text);
|
|
|
|
|
|
|
|
while( eix < e_length ) {
|
|
|
|
const [e_id, e_start, e_end] = emotes[eix];
|
|
|
|
|
|
|
|
// Does this emote go outside the bounds of this token?
|
|
|
|
if ( e_start > t_end || e_end > t_end ) {
|
|
|
|
// Output the remainder of this token.
|
|
|
|
if ( t_start === idx )
|
|
|
|
out.push(token);
|
|
|
|
else
|
|
|
|
out.push({
|
|
|
|
type: 'text',
|
|
|
|
text: text.slice(idx - t_start).join('')
|
|
|
|
});
|
|
|
|
|
|
|
|
// If this emote goes across token boundaries,
|
|
|
|
// skip it.
|
|
|
|
if ( e_start < t_end && e_end > t_end )
|
|
|
|
eix++;
|
|
|
|
|
|
|
|
idx = t_end;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-11-14 22:11:58 -05:00
|
|
|
// If this emote starts before the current index, skip it.
|
|
|
|
if ( e_start < idx ) {
|
|
|
|
eix++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
// If there's text at the beginning of the token that
|
|
|
|
// isn't part of this emote, output it.
|
|
|
|
if ( e_start > idx )
|
|
|
|
out.push({
|
|
|
|
type: 'text',
|
|
|
|
text: text.slice(idx - t_start, e_start - t_start).join('')
|
|
|
|
});
|
|
|
|
|
2017-12-13 17:35:20 -05:00
|
|
|
let src, srcSet;
|
|
|
|
|
|
|
|
const replacement = REPLACEMENTS[e_id];
|
2017-12-13 20:22:11 -05:00
|
|
|
if ( replacement && this.context.get('chat.fix-bad-emotes') ) {
|
2017-12-13 17:35:20 -05:00
|
|
|
src = `${REPLACEMENT_BASE}${replacement}`;
|
|
|
|
srcSet = '';
|
|
|
|
|
|
|
|
} else {
|
2018-04-09 19:57:05 -04:00
|
|
|
src = `${TWITCH_EMOTE_BASE}${e_id}/1.0`;
|
|
|
|
srcSet = `${TWITCH_EMOTE_BASE}${e_id}/1.0 1x, ${TWITCH_EMOTE_BASE}${e_id}/2.0 2x`;
|
2017-12-13 17:35:20 -05:00
|
|
|
}
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
out.push({
|
|
|
|
type: 'emote',
|
|
|
|
id: e_id,
|
|
|
|
provider: 'twitch',
|
2017-12-13 17:35:20 -05:00
|
|
|
src,
|
|
|
|
srcSet,
|
2017-11-13 01:23:39 -05:00
|
|
|
text: text.slice(e_start - t_start, e_end - t_start).join(''),
|
|
|
|
modifiers: []
|
|
|
|
});
|
|
|
|
|
|
|
|
idx = e_end;
|
|
|
|
eix++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We've finished processing emotes. If there is any
|
|
|
|
// remaining text in the token, push it out.
|
|
|
|
if ( idx < t_end ) {
|
|
|
|
if ( t_start === idx )
|
|
|
|
out.push(token);
|
|
|
|
else
|
|
|
|
out.push({
|
|
|
|
type: 'text',
|
|
|
|
text: text.slice(idx - t_start).join('')
|
|
|
|
});
|
|
|
|
|
|
|
|
idx = t_end;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
}
|