mirror of
https://github.com/urbit/shrub.git
synced 2024-12-20 17:32:11 +03:00
Merge pull request #3435 from tylershuster/rich-text-comments
links, publish: display rich text comments
This commit is contained in:
commit
2fbf988ad6
67
pkg/interface/src/logic/lib/tokenizeMessage.js
Normal file
67
pkg/interface/src/logic/lib/tokenizeMessage.js
Normal 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 };
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -221,6 +221,7 @@ export class LinkDetail extends Component {
|
||||
api={props.api}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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"
|
||||
>
|
||||
|
40
pkg/interface/src/views/components/RichText.js
Normal file
40
pkg/interface/src/views/components/RichText.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user