mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-29 07:45:33 +00:00
Add support for asynchronously loaded rich content blocks in chat.
This commit is contained in:
parent
9aafff3a14
commit
d1acc9f7cc
9 changed files with 419 additions and 5 deletions
|
@ -1,3 +1,8 @@
|
||||||
|
<div class="list-header">4.0.0-beta1.10<span>@77498dc31e57b48d0549</span> <time datetime="2018-04-03">(2018-04-03)</time></div>
|
||||||
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
|
<li>Added: Rendering support for rich content blocks in chat.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="list-header">4.0.0-beta1.9<span>@b27c86408c133765e687</span> <time datetime="2018-04-03">(2018-04-03)</time></div>
|
<div class="list-header">4.0.0-beta1.9<span>@b27c86408c133765e687</span> <time datetime="2018-04-03">(2018-04-03)</time></div>
|
||||||
<ul class="chat-menu-content menu-side-padding">
|
<ul class="chat-menu-content menu-side-padding">
|
||||||
<li>Added: Option to stop the player from automatically playing the recommended video after a video finishes.</li>
|
<li>Added: Option to stop the player from automatically playing the recommended video after a video finishes.</li>
|
||||||
|
|
|
@ -95,7 +95,7 @@ class FrankerFaceZ extends Module {
|
||||||
FrankerFaceZ.Logger = Logger;
|
FrankerFaceZ.Logger = Logger;
|
||||||
|
|
||||||
const VER = FrankerFaceZ.version_info = {
|
const VER = FrankerFaceZ.version_info = {
|
||||||
major: 4, minor: 0, revision: 0, extra: '-beta1.9',
|
major: 4, minor: 0, revision: 0, extra: '-beta1.10',
|
||||||
build: __webpack_hash__,
|
build: __webpack_hash__,
|
||||||
toString: () =>
|
toString: () =>
|
||||||
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`
|
||||||
|
|
20
src/modules/chat/clip_info.gql
Normal file
20
src/modules/chat/clip_info.gql
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
query FFZ_GetClipInfo($slug: ID!) {
|
||||||
|
clip(slug: $slug) {
|
||||||
|
id
|
||||||
|
curator {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
broadcaster {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
game {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
title
|
||||||
|
thumbnailURL(width: 86, height: 45)
|
||||||
|
viewCount
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import Emotes from './emotes';
|
||||||
import Room from './room';
|
import Room from './room';
|
||||||
import User from './user';
|
import User from './user';
|
||||||
import * as TOKENIZERS from './tokenizers';
|
import * as TOKENIZERS from './tokenizers';
|
||||||
|
import * as RICH_PROVIDERS from './rich_providers';
|
||||||
|
|
||||||
|
|
||||||
export default class Chat extends Module {
|
export default class Chat extends Module {
|
||||||
|
@ -45,11 +46,33 @@ export default class Chat extends Module {
|
||||||
this.tokenizers = {};
|
this.tokenizers = {};
|
||||||
this.__tokenizers = [];
|
this.__tokenizers = [];
|
||||||
|
|
||||||
|
this.rich_providers = {};
|
||||||
|
this.__rich_providers = [];
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Settings
|
// Settings
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
|
this.settings.add('chat.rich.enabled', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Appearance >> Rich Content',
|
||||||
|
title: 'Display rich content in chat.',
|
||||||
|
description: 'This displays rich content blocks for things like linked clips and videos.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('chat.rich.hide-tokens', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Appearance >> Rich Content',
|
||||||
|
title: 'Hide matching links for rich content.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('chat.scrollback-length', {
|
this.settings.add('chat.scrollback-length', {
|
||||||
default: 150,
|
default: 150,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -252,6 +275,10 @@ export default class Chat extends Module {
|
||||||
for(const key in TOKENIZERS)
|
for(const key in TOKENIZERS)
|
||||||
if ( has(TOKENIZERS, key) )
|
if ( has(TOKENIZERS, key) )
|
||||||
this.addTokenizer(TOKENIZERS[key]);
|
this.addTokenizer(TOKENIZERS[key]);
|
||||||
|
|
||||||
|
for(const key in RICH_PROVIDERS)
|
||||||
|
if ( has(RICH_PROVIDERS, key) )
|
||||||
|
this.addRichProvider(RICH_PROVIDERS[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -410,7 +437,22 @@ export default class Chat extends Module {
|
||||||
this.__tokenizers.sort((a, b) => {
|
this.__tokenizers.sort((a, b) => {
|
||||||
if ( a.priority > b.priority ) return -1;
|
if ( a.priority > b.priority ) return -1;
|
||||||
if ( a.priority < b.priority ) return 1;
|
if ( a.priority < b.priority ) return 1;
|
||||||
return a.type < b.sort;
|
return a.type < b.type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addRichProvider(provider) {
|
||||||
|
const type = provider.type;
|
||||||
|
this.rich_providers[type] = provider;
|
||||||
|
if ( provider.priority == null )
|
||||||
|
provider.priority = 0;
|
||||||
|
|
||||||
|
this.__rich_providers.push(provider);
|
||||||
|
this.__rich_providers.sort((a,b) => {
|
||||||
|
if ( a.priority > b.priority ) return -1;
|
||||||
|
if ( a.priority < b.priority ) return 1;
|
||||||
|
return a.type < b.type;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +467,22 @@ export default class Chat extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pluckRichContent(tokens) { // eslint-disable-line class-methods-use-this
|
||||||
|
if ( ! this.context.get('chat.rich.enabled') )
|
||||||
|
return;
|
||||||
|
|
||||||
|
const providers = this.__rich_providers;
|
||||||
|
|
||||||
|
for(const token of tokens) {
|
||||||
|
for(const provider of providers)
|
||||||
|
if ( provider.test.call(this, token) ) {
|
||||||
|
token.hidden = this.context.get('chat.rich.hide-tokens') && provider.hide_token;
|
||||||
|
return provider.process.call(this, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
tokenizeMessage(msg, user) {
|
tokenizeMessage(msg, user) {
|
||||||
if ( msg.content && ! msg.message )
|
if ( msg.content && ! msg.message )
|
||||||
msg.message = msg.content.text;
|
msg.message = msg.content.text;
|
||||||
|
|
138
src/modules/chat/rich_providers.js
Normal file
138
src/modules/chat/rich_providers.js
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Rich Content Providers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||||
|
//const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||||
|
|
||||||
|
import GET_CLIP from './clip_info.gql';
|
||||||
|
//import GET_VIDEO from './video_info.gql';
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Clips
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Clips = {
|
||||||
|
type: 'clip',
|
||||||
|
hide_token: true,
|
||||||
|
|
||||||
|
test(token) {
|
||||||
|
return token.type === 'link' && CLIP_URL.test(token.url)
|
||||||
|
},
|
||||||
|
|
||||||
|
process(token) {
|
||||||
|
const match = CLIP_URL.exec(token.url),
|
||||||
|
apollo = this.resolve('site.apollo');
|
||||||
|
|
||||||
|
if ( ! apollo || ! match )
|
||||||
|
return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
getData: async () => {
|
||||||
|
const result = await apollo.client.query({
|
||||||
|
query: GET_CLIP,
|
||||||
|
variables: {
|
||||||
|
slug: match[1]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
if ( game_name === 'creative' )
|
||||||
|
desc_1 = this.i18n.t('clip.desc.1.creative', '%{user} being Creative', {
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
else if ( game )
|
||||||
|
desc_1 = this.i18n.t('clip.desc.1.playing', '%{user} playing %{game}', {
|
||||||
|
user,
|
||||||
|
game: game_display
|
||||||
|
});
|
||||||
|
|
||||||
|
else
|
||||||
|
desc_1 = this.i18n.t('clip.desc.1', 'Clip of %{user}', {user});
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: token.url,
|
||||||
|
image: clip.thumbnailURL,
|
||||||
|
title: clip.title,
|
||||||
|
desc_1,
|
||||||
|
desc_2: this.i18n.t('clip.desc.2', 'Clipped by %{curator} — %{views|number} View%{views|en_plural}', {
|
||||||
|
curator: clip.curator.displayName,
|
||||||
|
views: clip.viewCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*export const Videos = {
|
||||||
|
type: 'video',
|
||||||
|
hide_token: true,
|
||||||
|
|
||||||
|
test(token) {
|
||||||
|
return token.type === 'link' && VIDEO_URL.test(token.url)
|
||||||
|
},
|
||||||
|
|
||||||
|
process(token) {
|
||||||
|
const match = VIDEO_URL.exec(token.url),
|
||||||
|
apollo = this.resolve('site.apollo');
|
||||||
|
|
||||||
|
if ( ! apollo || ! match )
|
||||||
|
return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
getData: async () => {
|
||||||
|
const result = await apollo.client.query({
|
||||||
|
query: GET_VIDEO,
|
||||||
|
variables: {
|
||||||
|
id: match[1]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
if ( game_name === 'creative' )
|
||||||
|
desc_1 = this.i18n.t('clip.desc.1.creative', '%{user} being Creative', {
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
else if ( game )
|
||||||
|
desc_1 = this.i18n.t('clip.desc.1.playing', '%{user} playing %{game}', {
|
||||||
|
user,
|
||||||
|
game: game_display
|
||||||
|
});
|
||||||
|
|
||||||
|
else
|
||||||
|
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_2: this.i18n.t('video.desc.2', '%{length} — %{views} Views - %{date}', {
|
||||||
|
length: video.lengthSeconds,
|
||||||
|
views: video.viewCount,
|
||||||
|
date: video.publishedAt
|
||||||
|
})/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
18
src/modules/chat/video_info.gql
Normal file
18
src/modules/chat/video_info.gql
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
query FFZ_GetVideoInfo($id: ID!) {
|
||||||
|
video(id: $id) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
previewThumbnailURL(width: 86, height: 45)
|
||||||
|
lengthSeconds
|
||||||
|
publishedAt
|
||||||
|
viewCount
|
||||||
|
game {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
owner {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import Twilight from 'site';
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
//import {Color} from 'utilities/color';
|
//import {Color} from 'utilities/color';
|
||||||
|
|
||||||
|
import RichContent from './rich_content';
|
||||||
|
|
||||||
const SUB_TIERS = {
|
const SUB_TIERS = {
|
||||||
1000: 1,
|
1000: 1,
|
||||||
2000: 2,
|
2000: 2,
|
||||||
|
@ -23,6 +25,7 @@ export default class ChatLine extends Module {
|
||||||
this.inject('chat');
|
this.inject('chat');
|
||||||
this.inject('site.fine');
|
this.inject('site.fine');
|
||||||
this.inject('site.web_munch');
|
this.inject('site.web_munch');
|
||||||
|
this.inject(RichContent);
|
||||||
|
|
||||||
this.ChatLine = this.fine.define(
|
this.ChatLine = this.fine.define(
|
||||||
'chat-line',
|
'chat-line',
|
||||||
|
@ -43,13 +46,16 @@ export default class ChatLine extends Module {
|
||||||
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
|
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
|
||||||
this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this);
|
this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this);
|
||||||
this.chat.context.on('changed:chat.rituals.show', this.updateLines, this);
|
this.chat.context.on('changed:chat.rituals.show', this.updateLines, this);
|
||||||
|
this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this);
|
||||||
|
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
|
||||||
|
|
||||||
const t = this,
|
const t = this,
|
||||||
React = this.web_munch.getModule('react');
|
React = this.web_munch.getModule('react');
|
||||||
if ( ! React )
|
if ( ! React )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const e = React.createElement;
|
const e = React.createElement,
|
||||||
|
FFZRichContent = this.rich_content && this.rich_content.RichContent;
|
||||||
|
|
||||||
this.ChatLine.ready(cls => {
|
this.ChatLine.ready(cls => {
|
||||||
cls.prototype.shouldComponentUpdate = function(props, state) {
|
cls.prototype.shouldComponentUpdate = function(props, state) {
|
||||||
|
@ -101,7 +107,12 @@ export default class ChatLine extends Module {
|
||||||
if ( ! msg.message && msg.messageParts )
|
if ( ! msg.message && msg.messageParts )
|
||||||
detokenizeMessage(msg);
|
detokenizeMessage(msg);
|
||||||
|
|
||||||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, {login: this.props.currentUserLogin, display: this.props.currentUserDisplayName});
|
const u = {
|
||||||
|
login: this.props.currentUserLogin,
|
||||||
|
display: this.props.currentUserDisplayName
|
||||||
|
},
|
||||||
|
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
|
||||||
|
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
|
||||||
|
|
||||||
let cls = 'chat-line__message',
|
let cls = 'chat-line__message',
|
||||||
out = (tokens.length || ! msg.ffz_type) ? [
|
out = (tokens.length || ! msg.ffz_type) ? [
|
||||||
|
@ -136,6 +147,8 @@ export default class ChatLine extends Module {
|
||||||
onClick: this.alwaysShowMessage
|
onClick: this.alwaysShowMessage
|
||||||
}, `<message deleted>`)),
|
}, `<message deleted>`)),
|
||||||
|
|
||||||
|
show && rich_content && e(FFZRichContent, rich_content),
|
||||||
|
|
||||||
/*this.state.renderDebug === 2 && e('div', {
|
/*this.state.renderDebug === 2 && e('div', {
|
||||||
className: 'border mg-t-05'
|
className: 'border mg-t-05'
|
||||||
}, old_render.call(this)),
|
}, old_render.call(this)),
|
||||||
|
|
155
src/sites/twitch-twilight/modules/chat/rich_content.jsx
Normal file
155
src/sites/twitch-twilight/modules/chat/rich_content.jsx
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RichContent Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import Module from 'utilities/module';
|
||||||
|
import {timeout} from 'utilities/object';
|
||||||
|
|
||||||
|
const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0';
|
||||||
|
|
||||||
|
export default class RichContent extends Module {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('i18n');
|
||||||
|
this.inject('site.web_munch');
|
||||||
|
|
||||||
|
this.RichContent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
|
const t = this,
|
||||||
|
React = this.web_munch.getModule('react');
|
||||||
|
if ( ! React )
|
||||||
|
return;
|
||||||
|
|
||||||
|
const createElement = React.createElement;
|
||||||
|
|
||||||
|
this.RichContent = class RichContent extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loaded: false,
|
||||||
|
error: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
try {
|
||||||
|
let data = this.props.getData();
|
||||||
|
if ( data instanceof Promise ) {
|
||||||
|
const to_wait = this.props.timeout || 1000;
|
||||||
|
if ( to_wait )
|
||||||
|
data = await timeout(data, to_wait);
|
||||||
|
else
|
||||||
|
data = await data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(Object.assign({
|
||||||
|
loaded: true
|
||||||
|
}, data));
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
this.setState({
|
||||||
|
loaded: true,
|
||||||
|
error: true,
|
||||||
|
title: t.i18n.t('card.error', 'An error occured.'),
|
||||||
|
desc_1: String(err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCardImage() {
|
||||||
|
return (<div class="chat-card__preview-img tw-c-background-alt-2 tw-align-items-center tw-flex tw-flex-shrink-0 tw-justify-content-center">
|
||||||
|
<div class="tw-card-img tw-flex-shrink-0 tw-flex tw-justify-content-center">
|
||||||
|
{this.state.error ?
|
||||||
|
(<img
|
||||||
|
class="chat-card__error-img"
|
||||||
|
data-test-selector="chat-card-error"
|
||||||
|
src={ERROR_IMAGE}
|
||||||
|
/>) :
|
||||||
|
(<figure class="tw-aspect tw-aspect--16x9 tw-aspect--align-top">
|
||||||
|
{this.state.loaded && this.state.image ?
|
||||||
|
(<img
|
||||||
|
class="tw-image"
|
||||||
|
src={this.state.image}
|
||||||
|
alt={this.state.title}
|
||||||
|
/>)
|
||||||
|
: null}
|
||||||
|
</figure>)}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCardDescription() {
|
||||||
|
let title = this.state.title,
|
||||||
|
desc_1 = this.state.desc_1,
|
||||||
|
desc_2 = this.state.desc_2;
|
||||||
|
|
||||||
|
if ( ! this.state.loaded ) {
|
||||||
|
desc_1 = t.i18n.t('card.loading', 'Loading...');
|
||||||
|
desc_2 = '';
|
||||||
|
title = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<div class={`tw-overflow-hidden tw-align-items-center tw-flex${desc_2 ? ' ffz--two-line' : ''}`}>
|
||||||
|
<div class="tw-full-width tw-pd-l-1">
|
||||||
|
<div class="chat-card__title tw-ellipsis">
|
||||||
|
<span
|
||||||
|
class="tw-font-size-5"
|
||||||
|
data-test-selector="chat-card-title"
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw-ellipsis">
|
||||||
|
<span
|
||||||
|
class="tw-c-text-alt-2 tw-font-size-6"
|
||||||
|
data-test-selector="chat-card-description"
|
||||||
|
title={desc_1}
|
||||||
|
>
|
||||||
|
{desc_1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{desc_2 && (<div class="tw-ellipsis">
|
||||||
|
<span
|
||||||
|
class="tw-c-text-alt-2 tw-font-size-6"
|
||||||
|
data-test-selector="chat-card-description"
|
||||||
|
title={desc_2}
|
||||||
|
>
|
||||||
|
{desc_2}
|
||||||
|
</span>
|
||||||
|
</div>)}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCard() {
|
||||||
|
return (<div class="ffz--chat-card tw-elevation-1 tw-mg-t">
|
||||||
|
<div class="tw-c-background tw-flex tw-flex-nowrap tw-pd-05">
|
||||||
|
{this.renderCardImage()}
|
||||||
|
{this.renderCardDescription()}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if ( ! this.state.url )
|
||||||
|
return this.renderCard();
|
||||||
|
|
||||||
|
return (<a
|
||||||
|
class="chat-card__link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferer noopener"
|
||||||
|
href={this.state.url}
|
||||||
|
>
|
||||||
|
{this.renderCard()}
|
||||||
|
</a>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,11 +19,18 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--chat-card {
|
||||||
|
.ffz--two-line {
|
||||||
|
.tw-ellipsis { line-height: 1.4rem }
|
||||||
|
.chat-card__title { line-height: 1.5rem }
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue