mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-05 13:55:54 +03:00
interface: refactored chat message component into smaller pieces
This commit is contained in:
parent
4d288f2e4e
commit
f57ad92701
28
pkg/interface/src/apps/chat/components/lib/content/code.js
Normal file
28
pkg/interface/src/apps/chat/components/lib/content/code.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
||||
export default class CodeContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
|
||||
const outputElement =
|
||||
(Boolean(content.code.output) &&
|
||||
content.code.output.length && content.code.output.length > 0) ?
|
||||
(
|
||||
<pre className={`f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb`}>
|
||||
{content.code.output[0].join('\n')}
|
||||
</pre>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="mv2">
|
||||
<pre className={`f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba`}>
|
||||
{content.code.expression}
|
||||
</pre>
|
||||
{outputElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
64
pkg/interface/src/apps/chat/components/lib/content/text.js
Normal file
64
pkg/interface/src/apps/chat/components/lib/content/text.js
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
const DISABLED_BLOCK_TOKENS = [
|
||||
'indentedCode',
|
||||
'blockquote',
|
||||
'atxHeading',
|
||||
'thematicBreak',
|
||||
'list',
|
||||
'setextHeading',
|
||||
'html',
|
||||
'definition',
|
||||
'table'
|
||||
];
|
||||
|
||||
const DISABLED_INLINE_TOKENS = [
|
||||
'autoLink',
|
||||
'url',
|
||||
'email',
|
||||
'link',
|
||||
'reference'
|
||||
];
|
||||
|
||||
const MessageMarkdown = React.memo(props => (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
plugins={[[
|
||||
RemarkDisableTokenizers,
|
||||
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
|
||||
]]} />
|
||||
));
|
||||
|
||||
|
||||
export default class TextContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
|
||||
const group = content.text.match(
|
||||
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/
|
||||
);
|
||||
if ((group !== null) // matched possible chatroom
|
||||
&& (group[2].length > 2) // possible ship?
|
||||
&& (urbitOb.isValidPatp(group[2]) // valid patp?
|
||||
&& (group[0] === content.text))) { // entire message is room name?
|
||||
return (
|
||||
<Link
|
||||
className="bb b--black b--white-d f7 mono lh-copy v-top"
|
||||
to={'/~groups/join/' + group.input}>
|
||||
{content.text}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<section className="chat-md-message">
|
||||
<MessageMarkdown source={content.text} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
101
pkg/interface/src/apps/chat/components/lib/content/url.js
Normal file
101
pkg/interface/src/apps/chat/components/lib/content/url.js
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
||||
const IMAGE_REGEX =
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/;
|
||||
|
||||
const YOUTUBE_REGEX =
|
||||
new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
export default class UrlContent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
const iframe = this.refs.iframe;
|
||||
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
const imgMatch = IMAGE_REGEX.exec(props.content.url);
|
||||
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
|
||||
|
||||
let contents = content.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={content.url}
|
||||
style={{
|
||||
width: '50%',
|
||||
maxWidth: '250px'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className={`f7 lh-copy v-top word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref="iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={content.url}
|
||||
className={`f7 lh-copy v-top bb b--white-d word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{content.url}</a>
|
||||
<a className="ml2 f7 pointer lh-copy v-top"
|
||||
onClick={e => this.unfoldEmbed()}>
|
||||
[embed]
|
||||
</a>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className={`f7 lh-copy v-top bb b--white-d b--black word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import UrlContent from './content/url';
|
||||
|
||||
|
||||
export default class MessageContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const content = props.letter;
|
||||
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} />;
|
||||
} else if ('url' in content) {
|
||||
return <UrlContent content={content} />;
|
||||
} else if ('me' in content) {
|
||||
return (
|
||||
<p className='f7 i lh-copy v-top'>
|
||||
{content.me}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
else if ('text' in content) {
|
||||
return <TextContent content={content} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,275 +1,127 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import MessageContent from './message-content';
|
||||
import { uxToHex, cite, writeText } from '../../../../lib/util';
|
||||
import moment from 'moment';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
const DISABLED_BLOCK_TOKENS = [
|
||||
'indentedCode',
|
||||
'blockquote',
|
||||
'atxHeading',
|
||||
'thematicBreak',
|
||||
'list',
|
||||
'setextHeading',
|
||||
'html',
|
||||
'definition',
|
||||
'table'
|
||||
];
|
||||
|
||||
const DISABLED_INLINE_TOKENS = [
|
||||
'autoLink',
|
||||
'url',
|
||||
'email',
|
||||
'link',
|
||||
'reference'
|
||||
];
|
||||
|
||||
const MessageMarkdown = React.memo(
|
||||
props => (<ReactMarkdown
|
||||
{...props}
|
||||
plugins={[[RemarkDisableTokenizers, { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }]]}
|
||||
/>));
|
||||
|
||||
export class Message extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unFoldEmbed = this.unFoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unFoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
const iframe = this.refs.iframe;
|
||||
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { props } = this;
|
||||
const letter = props.msg.letter;
|
||||
|
||||
if ('code' in letter) {
|
||||
const outputElement =
|
||||
(Boolean(letter.code.output) &&
|
||||
letter.code.output.length && letter.code.output.length > 0) ?
|
||||
(
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb">
|
||||
{letter.code.output[0].join('\n')}
|
||||
</pre>
|
||||
) : null;
|
||||
return (
|
||||
<div className="mv2">
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba">
|
||||
{letter.code.expression}
|
||||
</pre>
|
||||
{outputElement}
|
||||
</div>
|
||||
);
|
||||
} else if ('url' in letter) {
|
||||
const imgMatch =
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|svg|SVG)$/
|
||||
.exec(letter.url);
|
||||
const youTubeRegex = new RegExp(String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
const ytMatch =
|
||||
youTubeRegex.exec(letter.url);
|
||||
let contents = letter.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={letter.url}
|
||||
style={{
|
||||
height: 'min(250px, 20vh)',
|
||||
maxWidth: 'calc(100% - 36px - 1.5rem)',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className="f7 lh-copy v-top word-break-all"
|
||||
href={letter.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref="iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={letter.url}
|
||||
className="f7 lh-copy v-top bb b--white-d word-break-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{letter.url}
|
||||
</a>
|
||||
<a className="ml2 f7 pointer lh-copy v-top"
|
||||
onClick={e => this.unFoldEmbed()}
|
||||
>
|
||||
[embed]
|
||||
</a>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className="f7 lh-copy v-top bb b--white-d b--black word-break-all"
|
||||
href={letter.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if ('me' in letter) {
|
||||
return (
|
||||
<p className='f7 i lh-copy v-top'>
|
||||
{letter.me}
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
const group = letter.text.match(
|
||||
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/
|
||||
);
|
||||
if ((group !== null) // matched possible chatroom
|
||||
&& (group[2].length > 2) // possible ship?
|
||||
&& (urbitOb.isValidPatp(group[2]) // valid patp?
|
||||
&& (group[0] === letter.text))) { // entire message is room name?
|
||||
return (
|
||||
<Link
|
||||
className="bb b--black b--white-d f7 mono lh-copy v-top"
|
||||
to={'/~groups/join/' + group.input}
|
||||
>
|
||||
{letter.text}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<section className="chat-md-message">
|
||||
<MessageMarkdown
|
||||
source={letter.text}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const pending = props.msg.pending ? ' o-40' : '';
|
||||
const datestamp = '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
const containerClass =
|
||||
props.renderSigil ?
|
||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
||||
'w-100 pr3 cf hide-child flex' + pending;
|
||||
|
||||
const timestamp =
|
||||
moment.unix(props.msg.when / 1000).format(
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={containerClass}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
>
|
||||
{
|
||||
props.renderSigil ? (
|
||||
this.renderWithSigil(timestamp)
|
||||
) : (
|
||||
this.renderWithoutSigil(timestamp)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderWithSigil(timestamp) {
|
||||
const { props, state } = this;
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
|
||||
if (props.renderSigil) {
|
||||
const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm a');
|
||||
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = (contact.nickname.length > 0)
|
||||
? contact.nickname : `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={
|
||||
'w-100 f7 pl3 pt4 pr3 cf flex lh-copy ' + ' ' + pending
|
||||
}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
>
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div
|
||||
className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}
|
||||
>
|
||||
<div className="hide-child" style={paddingTop}>
|
||||
<p className="v-mid f9 gray2 dib mr3 c-default">
|
||||
<span
|
||||
className={'pointer ' + (contact.nickname || state.copied ? null : 'mono')}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 800);
|
||||
}}
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
{state.copied && 'Copied' || name}
|
||||
</span>
|
||||
</p>
|
||||
<p className="v-mid mono f9 gray2 dib">{timestamp}</p>
|
||||
<p className="v-mid mono f9 ml2 gray2 dib child dn-s">{datestamp}</p>
|
||||
</div>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'w-100 pr3 cf hide-child flex' + pending}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
>
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = (contact.nickname.length > 0)
|
||||
? contact.nickname : `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-100">
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
<div className="hide-child" style={paddingTop}>
|
||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
||||
<span
|
||||
className={
|
||||
'pointer ' +
|
||||
(contact.nickname || state.copied ? null : 'mono')
|
||||
}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => {
|
||||
this.setState({ copied: false });
|
||||
}, 800);
|
||||
}}
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
{state.copied && 'Copied' || name}
|
||||
</span>
|
||||
</p>
|
||||
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
|
||||
<p className={`v-mid mono f9 ml2 gray2 dib child dn-s`}>
|
||||
{datestamp}
|
||||
</p>
|
||||
</div>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderWithoutSigil(timestamp) {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className="flex w-100">
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
||||
style={{ flexGrow: 1 }}>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ function decodeGroup(group: Enc<Group>): Group {
|
||||
tags: decodeTags(group.tags),
|
||||
policy: decodePolicy(group.policy),
|
||||
};
|
||||
console.log(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user