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 { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil'; 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 { export class ChatInput extends Component {
constructor(props) { constructor(props) {
@ -52,7 +53,7 @@ export class ChatInput extends Component {
return { return {
me: letter me: letter
}; };
} else if (this.isUrl(letter)) { } else if (isUrl(letter)) {
return { return {
url: letter 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) { submit(text) {
const { props, state } = this; const { props, state } = this;
@ -91,58 +86,7 @@ export class ChatInput extends Component {
return; return;
} }
let messages = []; const messages = tokenizeMessage(text);
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);
}
props.deleteMessage(); 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) { uploadSuccess(url) {

View File

@ -3,6 +3,7 @@ import { Sigil } from '~/logic/lib/sigil';
import { cite } from '~/logic/lib/util'; import { cite } from '~/logic/lib/util';
import moment from 'moment'; import moment from 'moment';
import { Box, Text, Row } from '@tlon/indigo-react'; import { Box, Text, Row } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
export class CommentItem extends Component { export class CommentItem extends Component {
constructor(props) { constructor(props) {
@ -60,7 +61,7 @@ export class CommentItem extends Component {
</Text> </Text>
</Row> </Row>
</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> </Box>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,7 +56,7 @@ export default class RemoteContent extends Component<RemoteContentProps, RemoteC
wrapInLink(contents) { wrapInLink(contents) {
return (<a return (<a
href={this.props.url} 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" target="_blank"
rel="noopener noreferrer" 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;