Merge pull request #3435 from tylershuster/rich-text-comments

links, publish: display rich text comments
This commit is contained in:
matildepark 2020-09-05 16:14:20 -04:00 committed by GitHub
commit 2fbf988ad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 99 deletions

View File

@ -0,0 +1,67 @@
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
const isUrl = (string) => {
try {
return URL_REGEX.test(string);
} catch (e) {
return false;
}
}
const tokenizeMessage = (text) => {
let messages = [];
let message = [];
let isInCodeBlock = false;
let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) {
message.push('\n');
}
// A line of backticks enters and exits a codeblock
if (line.startsWith('```')) {
// But we need to check if we've ended a codeblock
endOfCodeBlock = isInCodeBlock;
isInCodeBlock = (!isInCodeBlock);
} else {
endOfCodeBlock = false;
}
if (isInCodeBlock || endOfCodeBlock) {
message.push(line);
} else {
line.split(/\s/).forEach((str) => {
if (
(str.startsWith('`') && str !== '`')
|| (str === '`' && !isInCodeBlock)
) {
isInCodeBlock = true;
} else if (
(str.endsWith('`') && str !== '`')
|| (str === '`' && isInCodeBlock)
) {
isInCodeBlock = false;
}
if (isUrl(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push(message);
message = [];
}
messages.push([str]);
message = [];
} else {
message.push(str);
}
});
}
});
if (message.length) {
// Add any remaining message
messages.push(message);
}
return messages;
};
export { tokenizeMessage as default, isUrl, URL_REGEX };

View File

@ -4,9 +4,10 @@ import { S3Upload } from '~/views/components/s3-upload'
;
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
export class ChatInput extends Component {
constructor(props) {
@ -52,7 +53,7 @@ export class ChatInput extends Component {
return {
me: letter
};
} else if (this.isUrl(letter)) {
} else if (isUrl(letter)) {
return {
url: letter
};
@ -63,13 +64,7 @@ export class ChatInput extends Component {
}
}
isUrl(string) {
try {
return URL_REGEX.test(string);
} catch (e) {
return false;
}
}
submit(text) {
const { props, state } = this;
@ -91,58 +86,7 @@ export class ChatInput extends Component {
return;
}
let messages = [];
let message = [];
let isInCodeBlock = false;
let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) {
message.push('\n');
}
// A line of backticks enters and exits a codeblock
if (line.startsWith('```')) {
// But we need to check if we've ended a codeblock
endOfCodeBlock = isInCodeBlock;
isInCodeBlock = (!isInCodeBlock);
} else {
endOfCodeBlock = false;
}
if (isInCodeBlock || endOfCodeBlock) {
message.push(line);
} else {
line.split(/\s/).forEach((str) => {
if (
(str.startsWith('`') && str !== '`')
|| (str === '`' && !isInCodeBlock)
) {
isInCodeBlock = true;
} else if (
(str.endsWith('`') && str !== '`')
|| (str === '`' && isInCodeBlock)
) {
isInCodeBlock = false;
}
if (this.isUrl(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push(message);
message = [];
}
messages.push([str]);
message = [];
} else {
message.push(str);
}
});
}
});
if (message.length) {
// Add any remaining message
messages.push(message);
}
const messages = tokenizeMessage(text);
props.deleteMessage();
@ -157,25 +101,6 @@ export class ChatInput extends Component {
);
}
});
// perf testing:
/*let closure = () => {
let x = 0;
for (var i = 0; i < 30; i++) {
x++;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{
text: `${x}`
}
);
}
setTimeout(closure, 1000);
};
this.closure = closure.bind(this);
setTimeout(this.closure, 2000);*/
}
uploadSuccess(url) {

View File

@ -3,6 +3,7 @@ import { Sigil } from '~/logic/lib/sigil';
import { cite } from '~/logic/lib/util';
import moment from 'moment';
import { Box, Text, Row } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
export class CommentItem extends Component {
constructor(props) {
@ -60,7 +61,7 @@ export class CommentItem extends Component {
</Text>
</Row>
</Row>
<Text display="block" py={3} fontSize={1}>{props.content}</Text>
<Text display="block" py={3} fontSize={1}><RichText remoteContentPolicy={props.remoteContentPolicy}>{props.content}</RichText></Text>
</Box>
);
}

View File

@ -40,7 +40,7 @@ export class Comments extends Component {
? props.comments.totalPages
: 1;
const { hideNicknames, hideAvatars } = props;
const { hideNicknames, hideAvatars, remoteContentPolicy } = props;
const commentsList = Object.keys(commentsPage)
.map((entry) => {
@ -68,6 +68,7 @@ export class Comments extends Component {
member={member}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
/>
);
});

View File

@ -221,6 +221,7 @@ export class LinkDetail extends Component {
api={props.api}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
/>
</div>
</div>

View File

@ -63,6 +63,7 @@ export default function PublishApp(props: PublishAppProps) {
associations,
hideNicknames,
hideAvatars,
remoteContentPolicy
} = props;
const active = location.pathname.endsWith("/~publish")
@ -161,6 +162,7 @@ export default function PublishApp(props: PublishAppProps) {
api={api}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
{...props}
/>
);

View File

@ -1,14 +1,13 @@
import React, { useState } from "react";
import moment from "moment";
import { Sigil } from "~/logic/lib/sigil";
import CommentInput from "./CommentInput";
import { uxToHex, cite } from "~/logic/lib/util";
import { Comment, NoteId } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { Button, Box, Row, Text } from "@tlon/indigo-react";
import { Box, Row } from "@tlon/indigo-react";
import styled from "styled-components";
import { Author } from "./Author";
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import RichText from '~/views/components/RichText';
const ClickBox = styled(Box)`
cursor: pointer;
@ -28,17 +27,11 @@ interface CommentItemProps {
}
export function CommentItem(props: CommentItemProps) {
const { ship, contacts, book, note, api } = props;
const { ship, contacts, book, note, api, remoteContentPolicy } = props;
const [editing, setEditing] = useState<boolean>(false);
const commentPath = Object.keys(props.comment)[0];
const commentData = props.comment[commentPath];
const content = commentData.content.split("\n").map((line, i) => {
return (
<Text className="mb2" key={i}>
{line}
</Text>
);
});
const content = tokenizeMessage(commentData.content).flat().join(' ');
const disabled = props.pending || window.ship !== commentData.author.slice(1);
@ -86,14 +79,13 @@ export function CommentItem(props: CommentItemProps) {
</Author>
</Row>
<Box mb={2}>
{!editing && content}
{editing && (
<CommentInput
{editing
? <CommentInput
onSubmit={onUpdate}
initial={commentData.content}
label="Update"
/>
)}
: <RichText className="f9 white-d" remoteContentPolicy={remoteContentPolicy}>{content}</RichText>}
</Box>
</Box>
);

View File

@ -8,6 +8,7 @@ import { Contacts } from "~/types/contact-update";
import _ from "lodash";
import GlobalApi from "~/logic/api/global";
import { FormikHelpers } from "formik";
import { LocalUpdateRemoteContentPolicy } from "~/types";
interface CommentsProps {
comments: Comment[];
@ -21,6 +22,7 @@ interface CommentsProps {
enabled: boolean;
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export function Comments(props: CommentsProps) {
@ -78,6 +80,7 @@ export function Comments(props: CommentsProps) {
pending={true}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
);
})}
@ -92,6 +95,7 @@ export function Comments(props: CommentsProps) {
note={note["note-id"]}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
))}
</Col>

View File

@ -13,6 +13,7 @@ import {
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { Author } from "./Author";
import { LocalUpdateRemoteContentPolicy } from "~/types";
interface NoteProps {
ship: string;
@ -24,6 +25,7 @@ interface NoteProps {
api: GlobalApi;
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export function Note(props: NoteProps & RouteComponentProps) {
@ -115,6 +117,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
)}
<Spinner

View File

@ -9,6 +9,7 @@ import { Contacts, Rolodex } from "../../../../types/contact-update";
import Notebook from "./Notebook";
import NewPost from "./new-post";
import { NoteRoutes } from './NoteRoutes';
import { LocalUpdateRemoteContentPolicy } from "~/types";
interface NotebookRoutesProps {
api: GlobalApi;
@ -21,6 +22,7 @@ interface NotebookRoutesProps {
groups: Groups;
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export function NotebookRoutes(
@ -74,6 +76,7 @@ export function NotebookRoutes(
contacts={notebookContacts}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
{...routeProps}
/>
);

View File

@ -56,7 +56,7 @@ export default class RemoteContent extends Component<RemoteContentProps, RemoteC
wrapInLink(contents) {
return (<a
href={this.props.url}
className={`f7 lh-copy v-top word-break-all ${(typeof contents === 'string') ? 'bb b--white-d b--black' : ''}`}
className={`word-break-all ${(typeof contents === 'string') ? 'bb b--white-d b--black' : ''}`}
target="_blank"
rel="noopener noreferrer"
>

View File

@ -0,0 +1,40 @@
import React from "react";
import RemoteContent from "~/views/components/RemoteContent";
import { hasProvider } from 'oembed-parser';
import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
const DISABLED_BLOCK_TOKENS = [
'indentedCode',
'atxHeading',
'thematicBreak',
'list',
'setextHeading',
'html',
'definition',
'table'
];
const DISABLED_INLINE_TOKENS = [];
const RichText = React.memo(({remoteContentPolicy, ...props}) => (
<ReactMarkdown
{...props}
renderers={{
link: (props) => {
if (hasProvider(props.href)) {
return <RemoteContent className="mw-100" url={props.href} remoteContentPolicy={remoteContentPolicy}/>;
}
return <a {...props} className="bb b--white-d b--black">{props.children}</a>
},
paragraph: (props) => {
return <p {...props} className="mb2 lh-copy">{props.children}</p>
}
}}
plugins={[[
RemarkDisableTokenizers,
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
]]} />
));
export default RichText;