Merge pull request #4809 from urbit/lf/publish-mp

publish: forestry
This commit is contained in:
matildepark 2021-05-05 21:38:22 -04:00 committed by GitHub
commit 61e9deb787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 574 additions and 193 deletions

Binary file not shown.

View File

@ -24,7 +24,6 @@
"formik": "^2.1.5",
"immer": "^8.0.1",
"lodash": "^4.17.20",
"markdown-to-jsx": "^6.11.4",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
@ -42,13 +41,15 @@
"react-use-gesture": "^9.1.3",
"react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1",
"remark": "^12.0.0",
"remark-breaks": "^2.0.1",
"remark-disable-tokenizers": "^1.0.24",
"remark-disable-tokenizers": "1.1.0",
"stacktrace-js": "^2.0.2",
"style-loader": "^1.3.0",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
"suncalc": "^1.8.0",
"unist-util-visit": "^3.0.0",
"urbit-ob": "^5.0.1",
"workbox-core": "^6.0.2",
"workbox-precaching": "^6.0.2",

View File

@ -3,6 +3,7 @@ import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import bigInt, { BigInteger } from 'big-integer';
import { buntPost } from '~/logic/lib/post';
import { unixToDa } from '~/logic/lib/util';
//import tokenizeMessage from './tokenizeMessage';
export function newPost(
title: string,
@ -19,6 +20,9 @@ export function newPost(
signatures: []
};
// re-enable on mainnet deploy
//const tokenisedBody = tokenizeMessage(body);
const revContainer: Post = { ...root, index: root.index + '/1' };
const commentsContainer = { ...root, index: root.index + '/2' };
@ -26,6 +30,7 @@ export function newPost(
...revContainer,
index: revContainer.index + '/1',
contents: [{ text: title }, { text: body }]
//contents: [{ text: title }, { text: body } ...tokenisedBody]
};
const nodes = {
@ -54,10 +59,13 @@ export function newPost(
export function editPost(rev: number, noteId: BigInteger, title: string, body: string) {
const now = Date.now();
// reenable
//const tokenisedBody = tokenizeMessage(body);
const newRev: Post = {
author: `~${window.ship}`,
index: `/${noteId.toString()}/1/${rev}`,
'time-sent': now,
//contents: [{ text: title }, ...tokenisedBody],
contents: [{ text: title }, { text: body }],
hash: null,
signatures: []
@ -85,8 +93,9 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
if (!rev) {
return empty;
}
const [title, body] = rev.post.contents as TextContent[];
return [revNum.toJSNumber(), title.text, body.text, rev.post];
const title = rev.post.contents[0];
const body = rev.post.contents.slice(1);
return [revNum.toJSNumber(), title.text, body, rev.post];
}
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
@ -113,10 +122,11 @@ export function getComments(node: GraphNode): GraphNode {
return comments;
}
export function getSnippet(body: string) {
const newlineIdx = body.indexOf('\n', 2);
const end = newlineIdx > -1 ? newlineIdx : body.length;
const start = body.substr(0, end);
export function getSnippet(body: any) {
const firstContent = Object.values(body[0])[0];
const newlineIdx = firstContent.indexOf('\n', 2);
const end = newlineIdx > -1 ? newlineIdx : firstContent.length;
const start = firstContent.substr(0, end);
return (start === body || start.startsWith('![')) ? start : `${start}...`;
return (start === firstContent || firstContent.startsWith('![')) ? start : `${start}...`;
}

View File

@ -3,6 +3,8 @@ import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks';
const URL_REGEX = new RegExp(String(/^(([\w\-\+]+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source));
const GROUP_REGEX = new RegExp(String(/^~[-a-z_]+\/[-a-z]+/.source));
const isUrl = (string) => {
try {
return URL_REGEX.test(string);
@ -15,17 +17,27 @@ const isRef = (str) => {
return isUrl(str) && str.startsWith('web+urbitgraph://');
};
const isGroup = str => {
try {
return GROUP_REGEX.test(str);
} catch (e) {
return false;
}
}
const convertToGroupRef = (group) => `web+urbitgraph://group/${group}`;
const tokenizeMessage = (text) => {
const messages = [];
let message = [];
let messages = [];
// by line
let currTextBlock = [];
let isInCodeBlock = false;
let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) {
message.push('\n');
}
// by space
let currTextLine = [];
// A line of backticks enters and exits a codeblock
if (line.startsWith('```')) {
if (line.trim().startsWith('```')) {
// But we need to check if we've ended a codeblock
endOfCodeBlock = isInCodeBlock;
isInCodeBlock = (!isInCodeBlock);
@ -34,9 +46,13 @@ const tokenizeMessage = (text) => {
}
if (isInCodeBlock || endOfCodeBlock) {
message.push(line);
currTextLine = [line];
} else {
line.split(/\s/).forEach((str) => {
const words = line.split(/\s/);
words.forEach((word, idx) => {
const str = isGroup(word) ? convertToGroupRef(word) : word;
const last = words.length - 1 === idx;
if (
(str.startsWith('`') && str !== '`')
|| (str === '`' && !isInCodeBlock)
@ -50,9 +66,12 @@ const tokenizeMessage = (text) => {
}
if(isRef(str) && !isInCodeBlock) {
if (message.length > 0) {
if (currTextLine.length > 0 || currTextBlock.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });
currTextLine.push('');
messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') });
currTextBlock = last ? [''] : [];
currTextLine = [];
}
const link = parsePermalink(str);
if(!link) {
@ -61,33 +80,39 @@ const tokenizeMessage = (text) => {
const reference = permalinkToReference(link);
messages.push(reference);
}
message = [];
currTextLine = [];
} else if (isUrl(str) && !isInCodeBlock) {
if (message.length > 0) {
if (currTextLine.length > 0 || currTextBlock.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });
message = [];
currTextLine.push('');
messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') });
currTextBlock = last ? [''] : [];
currTextLine = [];
}
messages.push({ url: str });
message = [];
currTextLine = [];
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) {
if (message.length > 0) {
if (currTextLine.length > 0 || currTextBlock.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') });
message = [];
currTextLine.push('');
messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') });
currTextBlock = last ? [''] : [];
currTextLine = [];
}
messages.push({ mention: str });
message = [];
currTextLine = [];
} else {
message.push(str);
currTextLine.push(str);
}
});
}
currTextBlock.push(currTextLine.join(' '))
});
if (message.length) {
if (currTextBlock.length) {
// Add any remaining message
messages.push({ text: message.join(' ') });
messages.push({ text: currTextBlock.join('\n') });
}
return messages;
};

View File

@ -22,7 +22,8 @@ import useLocalState from '~/logic/state/local';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { Dropdown } from '~/views/components/Dropdown';
import ProfileOverlay from '~/views/components/ProfileOverlay';
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
import { GraphContent} from '~/views/landscape/components/Graph/GraphContent';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -496,10 +497,10 @@ export const Message = React.memo(({
) : (
<></>
)}
<GraphContentWide
<GraphContent
{...bind}
width="100%"
post={msg}
contents={msg.contents}
transcluded={transcluded}
api={api}
showOurContact={showOurContact}

View File

@ -19,7 +19,7 @@ import {
GraphNotificationContents, GraphNotifIndex
} from '~/types';
import Author from '~/views/components/Author';
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { PermalinkEmbed } from '../permalinks/embed';
import { Header } from './header';
@ -150,7 +150,7 @@ export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
}
return (
<TruncBox truncate={8}>
<GraphContentWide api={{} as any} post={post} showOurContact />
<GraphContent api={{} as any} contents={post.contents} showOurContact />
</TruncBox>
);
};

View File

@ -8,7 +8,7 @@ import { getSnippet } from '~/logic/lib/publish';
import { useGroupForAssoc } from '~/logic/state/group';
import Author from '~/views/components/Author';
import { MentionText } from '~/views/components/MentionText';
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import ChatMessage from '../chat/components/ChatMessage';
import { NotePreviewContent } from '../publish/components/NotePreview';
import { PermalinkEmbed } from './embed';
@ -72,10 +72,10 @@ function TranscludedComment(props: {
group={group}
/>
<Box p="2">
<GraphContentWide
<GraphContent
api={api}
transcluded={transcluded}
post={comment.post}
contents={comment.post.contents}
showOurContact={false}
/>
</Box>
@ -111,7 +111,7 @@ function TranscludedPublishNode(props: {
</Text>
<Box p="2">
<NotePreviewContent
snippet={getSnippet(post?.post.contents[1]?.text)}
snippet={getSnippet(post?.post.contents.slice(1))}
/>
</Box>
</Col>

View File

@ -6,6 +6,7 @@ import { RouteComponentProps, useLocation } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { editPost, getLatestRevision } from '~/logic/lib/publish';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { PostForm, PostFormSchema } from './NoteForm';
interface EditPostProps {
@ -21,10 +22,27 @@ export function EditPost(props: EditPostProps & RouteComponentProps): ReactEleme
const [revNum, title, body] = getLatestRevision(note);
const location = useLocation();
let editContent = null;
editContent = body.reduce((val, curr) => {
if ('text' in curr) {
val = val + curr.text;
} else if ('mention' in curr) {
val = val + `~${curr.mention}`;
} else if ('url' in curr) {
val = val + curr.url;
} else if ('code' in curr) {
val = val + curr.code.expression;
} else if ('reference' in curr) {
val = `${val}${referenceToPermalink(curr).link}`;
}
return val;
}, '');
const waiter = useWaitForProps(props);
const initial: PostFormSchema = {
title,
body
body: editContent
};
const onSubmit = async (

View File

@ -1,12 +1,12 @@
import { Action, Anchor, Box, Col, Row, Text } from '@tlon/indigo-react';
import { Association, Graph, GraphNode, Group } from '@urbit/api';
import React, { useState, useEffect } from 'react';
import { Box, Text, Col, Anchor, Row, Action } from '@tlon/indigo-react';
import bigInt from 'big-integer';
import React, { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link, RouteComponentProps } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { roleForShip } from '~/logic/lib/group';
import { Contacts, GraphNode, Graph, Association, Unreads, Group, Post } from '@urbit/api';
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { getComments, getLatestRevision } from '~/logic/lib/publish';
import { useCopy } from '~/logic/lib/useCopy';
import { useQuery } from '~/logic/lib/useQuery';
@ -35,11 +35,10 @@ const renderers = {
}
};
export function NoteContent({ body }) {
export function NoteContent({ post, api }) {
return (
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
<GraphContent tall contents={post.contents.slice(1)} showOurContact api={api} />
</Box>
);
}
@ -124,7 +123,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
</Author>
</Row>
</Col>
<NoteContent body={body} />
<NoteContent api={props.api} post={post} />
<NoteNavigation
notebook={notebook}
noteId={noteId}

View File

@ -137,46 +137,6 @@
display: none;
}
.md h1, .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul, .md ol, .md blockquote,.md code,.md pre {
font-size: 14px;
margin-bottom: 16px;
}
.md ul ul {
margin-bottom: 0px;
}
.md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul {
font-weight: 400;
}
.md h1 {
font-weight: 600;
}
.md h2, .md h3, .md h4, .md h5 {
color:var(--gray);
}
.md {
line-height: 1.5;
}
.md code, .md pre {
font-family: "Source Code Pro", mono;
white-space: pre-wrap;
}
.md ul>li, .md ol>li {
line-height: 1.5;
}
.md a {
border-bottom-style: solid;
border-bottom-width: 1px;
}
.md img {
margin-bottom: 8px;
}
@media all and (prefers-color-scheme: dark) {
.options.open {
background-color: #4d4d4d;

View File

@ -11,8 +11,8 @@ import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import { getLatestCommentRevision } from '~/logic/lib/publish';
import { useCopy } from '~/logic/lib/useCopy';
import useMetadataState from '~/logic/state/metadata';
import { GraphContent } from '../landscape/components/Graph/GraphContent';
import Author from '~/views/components/Author';
import { GraphContentWide } from '../landscape/components/Graph/GraphContentWide';
const ClickBox = styled(Box)`
cursor: pointer;
@ -31,14 +31,13 @@ interface CommentItemProps {
highlighted: boolean;
}
export function CommentItem(props: CommentItemProps): ReactElement {
export function CommentItem(props: CommentItemProps) {
let { highlighted } = props;
const { ship, name, api, comment, group } = props;
const association = useMetadataState(
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
);
const ref = useRef<HTMLElement | null>(null);
console.log(comment);
const ref = useRef<HTMLDivElement>(null);
const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending;
@ -131,14 +130,14 @@ export function CommentItem(props: CommentItemProps): ReactElement {
</Row>
</Author>
</Row>
<GraphContentWide
<GraphContent
borderRadius="1"
p="1"
mb="1"
backgroundColor={highlighted ? 'washedBlue' : 'white'}
transcluded={0}
api={api}
post={post}
contents={post.contents}
showOurContact
/>
</Box>

View File

@ -0,0 +1,395 @@
import React from 'react';
import _ from 'lodash';
import { Post, Content, ReferenceContent } from '@urbit/api';
import {
Box,
Text,
Anchor,
H1,
H2,
H3,
H4,
Row,
Col,
} from '@tlon/indigo-react';
import GlobalApi from '~/logic/api/global';
import TextContent from './content/text';
import CodeContent from './content/code';
import RemoteContent from '~/views/components/RemoteContent';
import { Mention } from '~/views/components/MentionText';
import { PermalinkEmbed } from '~/views/apps/permalinks/embed';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { GraphContentWide } from './GraphContentWide';
import { PropFunc } from '~/types';
import fromMarkdown from 'mdast-util-from-markdown';
import Dot from '~/views/components/Dot';
import {
Root,
Parent,
Content as AstContent,
BlockContent,
} from '@types/mdast';
import { parseTall, parseWide } from './parse';
type StitchMode = 'merge' | 'block' | 'inline';
// XX make better
type GraphAstNode = any;
interface GraphMentionNode {
type: 'graph-mention';
ship: string;
}
interface GraphRefereceNode {
type: 'graph-reference';
reference: ReferenceContent;
}
interface GraphUrl {
type: 'graph-url';
url: string;
}
const codeToMdAst = (content: CodeContent) => {
return {
type: 'root',
children: [
{
type: 'code',
value: content.code.expression
},
{
type: 'code',
value: (content.code.output || []).join('\n')
}
]
};
}
const contentToMdAst = (tall: boolean) => (
content: Content
): [StitchMode, any] => {
if ('text' in content) {
return ['merge', tall ? parseTall(content.text) : parseWide(content.text)] as [StitchMode, any];
} else if ('code' in content) {
return [
'block',
codeToMdAst(content)
];
} else if ('reference' in content) {
return [
'block',
{
type: 'root',
children: [
{
type: 'graph-reference',
reference: content.reference,
},
],
},
];
} else if ('url' in content) {
return [
'block',
{
type: 'root',
children: [
{
type: 'graph-url',
url: content.url,
},
],
},
];
} else if ('mention' in content) {
return [
'inline',
{
type: 'root',
children: [
{
type: 'graph-mention',
ship: content.mention,
},
],
},
];
}
return [
'inline',
{
type: 'root',
children: [],
},
];
};
function stitchInline(a: any, b: any) {
if (!a?.children) {
throw new Error('Bad stitchInline call: missing root');
}
const lastParaIdx = a.children.length - 1;
const last = a.children[lastParaIdx];
if (last?.children) {
const ros = {
...a,
children: [
...a.children.slice(0, lastParaIdx),
stitchInline(last, b),
...a.children.slice(lastParaIdx + 1),
],
};
return ros;
}
const res = { ...a, children: [...a.children, ...b] };
return res;
}
function last<T>(arr: T[]) {
return arr[arr.length - 1];
}
function getChildren<T extends {}>(node: T): AstContent[] {
if ('children' in node) {
// @ts-ignore
return node.children;
}
return [];
}
export function asParent<T extends BlockContent>(node: T): Parent | undefined {
return ['paragraph', 'heading', 'list', 'listItem', 'table'].includes(
node.type
)
? (node as Parent)
: undefined;
}
function stitchMerge(a: Root, b: Root) {
const aChildren = a.children;
const bChildren = b.children;
if (last(aChildren)?.type === bChildren[0]?.type) {
const aGrandchild = getChildren(last(aChildren));
const bGrandchild = getChildren(bChildren[0]);
const mergedPara = {
...last(aChildren),
children: [...aGrandchild, ...bGrandchild],
};
return {
...a,
children: [...aChildren.slice(0, -1), mergedPara, ...bChildren.slice(1)],
};
}
return { ...a, children: [...aChildren, ...bChildren] };
}
function stitchBlock(a: Root, b: AstContent[]) {
return { ...a, children: [...a.children, ...b] };
}
function stitchInlineAfterBlock(a: Root, b: GraphMentionNode[]) {
return {
...a,
children: [...a.children, { type: 'paragraph', children: b }],
};
}
function stitchAsts(asts: [StitchMode, GraphAstNode][]) {
return _.reduce(
asts.slice(1),
([prevMode, ast], [mode, val]): [StitchMode, GraphAstNode] => {
if (prevMode === 'block') {
if (mode === 'inline') {
return [mode, stitchInlineAfterBlock(ast, val?.children ?? [])];
}
if (mode === 'merge') {
return [mode, stitchBlock(ast, val?.children ?? [])];
}
if (mode === 'block') {
return [mode, stitchBlock(ast, val?.children ?? [])];
}
}
if (mode === 'inline') {
return [mode, stitchInline(ast, val?.children ?? [])];
}
if (mode === 'merge') {
return [mode, stitchMerge(ast, val)];
}
if (mode === 'block') {
return [mode, stitchBlock(ast, val?.children ?? [])];
}
return [mode, ast];
},
asts[0]
);
}
const header = ({ children, depth, ...rest }) => {
const level = depth;
const inner =
level === 1 ? (
<H1>{children}</H1>
) : level === 2 ? (
<H2>{children}</H2>
) : level === 3 ? (
<H3>{children}</H3>
) : (
<H4>{children}</H4>
);
return (
<Box {...rest} mt="2" mb="4">
{inner}
</Box>
);
};
const renderers = {
heading: header,
inlineCode: ({ language, value }) => {
return (
<Text
mono
p="1"
backgroundColor="washedGray"
fontSize="0"
style={{ whiteSpace: 'pre-wrap' }}
>
{value}
</Text>
);
},
blockquote: ({ children, tall, ...rest }) => {
return (
<Text
lineHeight="20px"
display="block"
borderLeft="1px solid"
color="black"
paddingLeft={2}
py="1"
>
{children}
</Text>
);
},
paragraph: ({ children }) => {
return (
<Text fontSize="1" lineHeight={'20px'}>
{children}
</Text>
);
},
listItem: ({ children }) => {
return (
<Box position="relative" alignItems="center">
<Dot
top="7px"
position="absolute"
left="0px"
mr="1"
height="20px"
width="20px"
/>
<Box ml="2">{children}</Box>
</Box>
);
},
code: ({ language, tall, value, ...rest }) => {
console.log(rest);
const inner = (
<Text
p="1"
className="clamp-message"
display="block"
borderRadius="1"
mono
fontSize="0"
backgroundColor="washedGray"
overflowX="auto"
style={{ whiteSpace: 'pre' }}
>
{value}
</Text>
);
return tall ? <Box mb="2">{inner}</Box> : inner;
},
link: (props) => {
return (
<Anchor href={props.href} borderBottom="1" color="black">
{props.children}
</Anchor>
);
},
list: ({ depth, children }) => {
return (
<Col ml="3" gapY="2" my="2">
{children}
</Col>
);
},
'graph-mention': ({ ship }) => <Mention api={{} as any} ship={ship} />,
'graph-url': ({ url }) => (
<Box my="2" flexShrink={0}>
<RemoteContent key={url} url={url} />
</Box>
),
'graph-reference': ({ api, reference }) => {
const { link } = referenceToPermalink({ reference });
return (
<PermalinkEmbed api={api} link={link} transcluded={0} showOurContact />
);
},
root: ({ children }) => <Col gapY="2">{children}</Col>,
text: ({ value }) => value,
};
export function Graphdown<T extends {} = {}>(
props: {
ast: GraphAstNode;
tall?: boolean;
depth?: number;
} & T
) {
const { ast, depth = 0, ...rest } = props;
const { type, children = [], ...nodeRest } = ast;
const Renderer = renderers[ast.type] ?? (() => `unknown element: ${type}`);
return (
<Renderer depth={depth} {...rest} {...nodeRest}>
{children.map((c) => (
<Graphdown depth={depth+1} {...rest} ast={c} />
))}
</Renderer>
);
}
export const GraphContent = React.memo(function GraphContent(
props: {
tall?: boolean;
transcluded?: number;
contents: Content[];
api: GlobalApi;
showOurContact: boolean;
} & PropFunc<typeof Box>
) {
const {
post,
contents,
tall = false,
transcluded = 0,
showOurContact,
api,
...rest
} = props;
const [,ast] = stitchAsts(contents.map(contentToMdAst(tall)));
return (
<Box {...rest}>
<Graphdown api={api} ast={ast} />
</Box>
);
});

View File

@ -1,81 +0,0 @@
import { Box } from '@tlon/indigo-react';
import { Content, Post, ReferenceContent } from '@urbit/api';
import React from 'react';
import GlobalApi from '~/logic/api/global';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { PropFunc } from '~/types';
import { PermalinkEmbed } from '~/views/apps/permalinks/embed';
import { Mention } from '~/views/components/MentionText';
import RemoteContent from '~/views/components/RemoteContent';
import CodeContent from './content/code';
import TextContent from './content/text';
function GraphContentWideInner(
props: {
transcluded?: number;
post: Post;
api: GlobalApi;
showOurContact: boolean;
} & PropFunc<typeof Box>
) {
const { post, transcluded = 0, showOurContact, api, ...rest } = props;
return (
<Box {...rest}>
{post.contents.map((content: Content, i) => {
if ('text' in content) {
return (
<TextContent
key={i}
api={api}
fontSize={1}
lineHeight={'20px'}
content={content}
/>
);
} else if ('code' in content) {
return <CodeContent key={i} content={content} />;
} else if ('reference' in content) {
const { link } = referenceToPermalink(content as ReferenceContent);
return (
<PermalinkEmbed
link={link}
api={api}
transcluded={transcluded}
showOurContact={showOurContact}
/>
);
} else if ('url' in content) {
return (
<Box
key={i}
flexShrink={0}
fontSize={1}
lineHeight="20px"
color="black"
width="fit-content"
maxWidth="min(500px, 100%)"
>
<RemoteContent
key={content.url}
// @ts-ignore Invalid prop detection due to double with() export
url={content.url}
transcluded={transcluded}
/>
</Box>
);
} else if ('mention' in content) {
const first = i => i === 0;
return (<Mention
key={i}
first={first(i)}
ship={content.mention}
api={api}
/>);
}
})}
</Box>
);
}
export const GraphContentWide = React.memo(GraphContentWideInner);

View File

@ -75,21 +75,39 @@ const renderers = {
);
},
link: (props) => {
return <Anchor src={props.href} borderBottom="1" color="black">{props.children}</Anchor>;
return <Anchor src={props.href} borderBottom="1" color="black">{props.children}</Anchor>
},
list: ({depth, children}) => {
return <Text my='2' display='block' fontSize='1' ml={depth ? (2 * depth) : 0} lineHeight={'20px'}>{children}</Text>
}
};
const MessageMarkdown = React.memo((props) => {
const { source, ...rest } = props;
const { source, allowHeaders, allowLists, ...rest } = props;
const blockCode = source.split('```');
const codeLines = blockCode.map(codes => codes.split('\n'));
const lines = codeLines.reduce((acc, val, i) => {
const codeLines = blockCode.map((codes) => codes.split('\n'));
let lines = [];
if (allowLists) {
lines.push(source);
} else {
lines = codeLines.reduce((acc, val, i) => {
if (i % 2 === 1) {
return [...acc, `\`\`\`${val.join('\n')}\`\`\``];
} else {
return [...acc, ...val];
}
}, []);
}
const modifiedBlockTokens = DISABLED_BLOCK_TOKENS.filter(e => {
if (allowHeaders && allowLists) {
return (e in ["setextHeading", "atxHeading", "list"])
} else if (allowHeaders) {
return (e in ["setextHeading", "atxHeading"])
} else if (allowLists) {
return (e === "list")
}
})
return lines.map((line, i) => (
<React.Fragment key={i}>
@ -118,7 +136,7 @@ const MessageMarkdown = React.memo((props) => {
[
RemarkDisableTokenizers,
{
block: DISABLED_BLOCK_TOKENS,
block: modifiedBlockTokens,
inline: DISABLED_INLINE_TOKENS
}
]
@ -130,23 +148,26 @@ const MessageMarkdown = React.memo((props) => {
export default function TextContent(props) {
const content = props.content;
const allowHeaders = props.allowHeaders;
const allowLists = props.allowLists;
const group = content.text.match(
const group = content.text.trim().match(
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/
);
const isGroupLink =
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?
group[0] === content.text.trim(); // entire message is room name?
if (isGroupLink) {
const resource = `/ship/${content.text}`;
const resource = `/ship/${content.text.trim()}`;
return (
<GroupLink
resource={resource}
api={props.api}
pl='2'
my='2'
border='1'
borderRadius='2'
borderColor='washedGray'
@ -161,7 +182,7 @@ export default function TextContent(props) {
lineHeight={props.lineHeight ? props.lineHeight : '20px'}
style={{ overflowWrap: 'break-word' }}
>
<MessageMarkdown source={content.text} />
<MessageMarkdown source={content.text} allowHeaders={allowHeaders} allowLists={allowLists} />
</Text>
);
}

View File

@ -0,0 +1,33 @@
import remark from 'remark';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
const DISABLED_BLOCK_TOKENS = [
'indentedCode',
'atxHeading',
'thematicBreak',
'list',
'setextHeading',
'html',
'definition',
'table',
];
const DISABLED_INLINE_TOKENS = ['autoLink', 'url', 'email', 'reference'];
const tallParser = remark().freeze();
export const parseTall = (text: string) => tallParser.parse(text);
const wideParser = remark()
.use([
[
RemarkDisableTokenizers,
{
block: DISABLED_BLOCK_TOKENS,
inline: DISABLED_INLINE_TOKENS,
},
],
])
.freeze();
export const parseWide = (text: string) => wideParser.parse(text);

View File

@ -3,7 +3,7 @@ import { Post } from '@urbit/api';
import React, { ReactElement } from 'react';
import styled from 'styled-components';
import GlobalApi from '~/logic/api/global';
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
const TruncatedBox = styled(Col)`
display: -webkit-box;
@ -31,9 +31,9 @@ const PostContent = (props: PostContentProps): ReactElement => {
textOverflow="ellipsis"
overflow="hidden"
>
<GraphContentWide
<GraphContent
transcluded={0}
post={post}
contents={post.contents}
api={api}
showOurContact
/>