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", "formik": "^2.1.5",
"immer": "^8.0.1", "immer": "^8.0.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"markdown-to-jsx": "^6.11.4",
"moment": "^2.29.1", "moment": "^2.29.1",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
@ -42,13 +41,15 @@
"react-use-gesture": "^9.1.3", "react-use-gesture": "^9.1.3",
"react-virtuoso": "^0.20.3", "react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1", "react-visibility-sensor": "^5.1.1",
"remark": "^12.0.0",
"remark-breaks": "^2.0.1", "remark-breaks": "^2.0.1",
"remark-disable-tokenizers": "^1.0.24", "remark-disable-tokenizers": "1.1.0",
"stacktrace-js": "^2.0.2", "stacktrace-js": "^2.0.2",
"style-loader": "^1.3.0", "style-loader": "^1.3.0",
"styled-components": "^5.1.1", "styled-components": "^5.1.1",
"styled-system": "^5.1.5", "styled-system": "^5.1.5",
"suncalc": "^1.8.0", "suncalc": "^1.8.0",
"unist-util-visit": "^3.0.0",
"urbit-ob": "^5.0.1", "urbit-ob": "^5.0.1",
"workbox-core": "^6.0.2", "workbox-core": "^6.0.2",
"workbox-precaching": "^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 bigInt, { BigInteger } from 'big-integer';
import { buntPost } from '~/logic/lib/post'; import { buntPost } from '~/logic/lib/post';
import { unixToDa } from '~/logic/lib/util'; import { unixToDa } from '~/logic/lib/util';
//import tokenizeMessage from './tokenizeMessage';
export function newPost( export function newPost(
title: string, title: string,
@ -19,6 +20,9 @@ export function newPost(
signatures: [] signatures: []
}; };
// re-enable on mainnet deploy
//const tokenisedBody = tokenizeMessage(body);
const revContainer: Post = { ...root, index: root.index + '/1' }; const revContainer: Post = { ...root, index: root.index + '/1' };
const commentsContainer = { ...root, index: root.index + '/2' }; const commentsContainer = { ...root, index: root.index + '/2' };
@ -26,6 +30,7 @@ export function newPost(
...revContainer, ...revContainer,
index: revContainer.index + '/1', index: revContainer.index + '/1',
contents: [{ text: title }, { text: body }] contents: [{ text: title }, { text: body }]
//contents: [{ text: title }, { text: body } ...tokenisedBody]
}; };
const nodes = { const nodes = {
@ -54,10 +59,13 @@ export function newPost(
export function editPost(rev: number, noteId: BigInteger, title: string, body: string) { export function editPost(rev: number, noteId: BigInteger, title: string, body: string) {
const now = Date.now(); const now = Date.now();
// reenable
//const tokenisedBody = tokenizeMessage(body);
const newRev: Post = { const newRev: Post = {
author: `~${window.ship}`, author: `~${window.ship}`,
index: `/${noteId.toString()}/1/${rev}`, index: `/${noteId.toString()}/1/${rev}`,
'time-sent': now, 'time-sent': now,
//contents: [{ text: title }, ...tokenisedBody],
contents: [{ text: title }, { text: body }], contents: [{ text: title }, { text: body }],
hash: null, hash: null,
signatures: [] signatures: []
@ -85,8 +93,9 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
if (!rev) { if (!rev) {
return empty; return empty;
} }
const [title, body] = rev.post.contents as TextContent[]; const title = rev.post.contents[0];
return [revNum.toJSNumber(), title.text, body.text, rev.post]; const body = rev.post.contents.slice(1);
return [revNum.toJSNumber(), title.text, body, rev.post];
} }
export function getLatestCommentRevision(node: GraphNode): [number, Post] { export function getLatestCommentRevision(node: GraphNode): [number, Post] {
@ -113,10 +122,11 @@ export function getComments(node: GraphNode): GraphNode {
return comments; return comments;
} }
export function getSnippet(body: string) { export function getSnippet(body: any) {
const newlineIdx = body.indexOf('\n', 2); const firstContent = Object.values(body[0])[0];
const end = newlineIdx > -1 ? newlineIdx : body.length; const newlineIdx = firstContent.indexOf('\n', 2);
const start = body.substr(0, end); 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 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) => { const isUrl = (string) => {
try { try {
return URL_REGEX.test(string); return URL_REGEX.test(string);
@ -15,17 +17,27 @@ const isRef = (str) => {
return isUrl(str) && str.startsWith('web+urbitgraph://'); 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 tokenizeMessage = (text) => {
const messages = []; let messages = [];
let message = []; // by line
let currTextBlock = [];
let isInCodeBlock = false; let isInCodeBlock = false;
let endOfCodeBlock = false; let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line, index) => { text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) { // by space
message.push('\n'); let currTextLine = [];
}
// A line of backticks enters and exits a codeblock // 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 // But we need to check if we've ended a codeblock
endOfCodeBlock = isInCodeBlock; endOfCodeBlock = isInCodeBlock;
isInCodeBlock = (!isInCodeBlock); isInCodeBlock = (!isInCodeBlock);
@ -34,9 +46,13 @@ const tokenizeMessage = (text) => {
} }
if (isInCodeBlock || endOfCodeBlock) { if (isInCodeBlock || endOfCodeBlock) {
message.push(line); currTextLine = [line];
} else { } 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 ( if (
(str.startsWith('`') && str !== '`') (str.startsWith('`') && str !== '`')
|| (str === '`' && !isInCodeBlock) || (str === '`' && !isInCodeBlock)
@ -50,9 +66,12 @@ const tokenizeMessage = (text) => {
} }
if(isRef(str) && !isInCodeBlock) { 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 // 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); const link = parsePermalink(str);
if(!link) { if(!link) {
@ -61,33 +80,39 @@ const tokenizeMessage = (text) => {
const reference = permalinkToReference(link); const reference = permalinkToReference(link);
messages.push(reference); messages.push(reference);
} }
message = []; currTextLine = [];
} else if (isUrl(str) && !isInCodeBlock) { } 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 // If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') }); currTextLine.push('');
message = []; messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') });
currTextBlock = last ? [''] : [];
currTextLine = [];
} }
messages.push({ url: str }); messages.push({ url: str });
message = []; currTextLine = [];
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) { } 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 // If we're in the middle of a message, add it to the stack and reset
messages.push({ text: message.join(' ') }); currTextLine.push('');
message = []; messages.push({ text: currTextBlock.join('\n') + currTextLine.join(' ') });
currTextBlock = last ? [''] : [];
currTextLine = [];
} }
messages.push({ mention: str }); messages.push({ mention: str });
message = []; currTextLine = [];
} else { } else {
message.push(str); currTextLine.push(str);
} }
}); });
} }
currTextBlock.push(currTextLine.join(' '))
}); });
if (message.length) { if (currTextBlock.length) {
// Add any remaining message // Add any remaining message
messages.push({ text: message.join(' ') }); messages.push({ text: currTextBlock.join('\n') });
} }
return messages; return messages;
}; };

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { RouteComponentProps, useLocation } from 'react-router-dom';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { editPost, getLatestRevision } from '~/logic/lib/publish'; import { editPost, getLatestRevision } from '~/logic/lib/publish';
import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { PostForm, PostFormSchema } from './NoteForm'; import { PostForm, PostFormSchema } from './NoteForm';
interface EditPostProps { interface EditPostProps {
@ -21,10 +22,27 @@ export function EditPost(props: EditPostProps & RouteComponentProps): ReactEleme
const [revNum, title, body] = getLatestRevision(note); const [revNum, title, body] = getLatestRevision(note);
const location = useLocation(); 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 waiter = useWaitForProps(props);
const initial: PostFormSchema = { const initial: PostFormSchema = {
title, title,
body body: editContent
}; };
const onSubmit = async ( const onSubmit = async (

View File

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

View File

@ -137,46 +137,6 @@
display: none; 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) { @media all and (prefers-color-scheme: dark) {
.options.open { .options.open {
background-color: #4d4d4d; background-color: #4d4d4d;

View File

@ -11,8 +11,8 @@ import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import { getLatestCommentRevision } from '~/logic/lib/publish'; import { getLatestCommentRevision } from '~/logic/lib/publish';
import { useCopy } from '~/logic/lib/useCopy'; import { useCopy } from '~/logic/lib/useCopy';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import { GraphContent } from '../landscape/components/Graph/GraphContent';
import Author from '~/views/components/Author'; import Author from '~/views/components/Author';
import { GraphContentWide } from '../landscape/components/Graph/GraphContentWide';
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
cursor: pointer; cursor: pointer;
@ -31,14 +31,13 @@ interface CommentItemProps {
highlighted: boolean; highlighted: boolean;
} }
export function CommentItem(props: CommentItemProps): ReactElement { export function CommentItem(props: CommentItemProps) {
let { highlighted } = props; let { highlighted } = props;
const { ship, name, api, comment, group } = props; const { ship, name, api, comment, group } = props;
const association = useMetadataState( const association = useMetadataState(
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name]) useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
); );
const ref = useRef<HTMLElement | null>(null); const ref = useRef<HTMLDivElement>(null);
console.log(comment);
const [, post] = getLatestCommentRevision(comment); const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending; const disabled = props.pending;
@ -131,14 +130,14 @@ export function CommentItem(props: CommentItemProps): ReactElement {
</Row> </Row>
</Author> </Author>
</Row> </Row>
<GraphContentWide <GraphContent
borderRadius="1" borderRadius="1"
p="1" p="1"
mb="1" mb="1"
backgroundColor={highlighted ? 'washedBlue' : 'white'} backgroundColor={highlighted ? 'washedBlue' : 'white'}
transcluded={0} transcluded={0}
api={api} api={api}
post={post} contents={post.contents}
showOurContact showOurContact
/> />
</Box> </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) => { 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 MessageMarkdown = React.memo((props) => {
const { source, ...rest } = props; const { source, allowHeaders, allowLists, ...rest } = props;
const blockCode = source.split('```'); const blockCode = source.split('```');
const codeLines = blockCode.map(codes => codes.split('\n')); const codeLines = blockCode.map((codes) => codes.split('\n'));
const lines = codeLines.reduce((acc, val, i) => { let lines = [];
if (i % 2 === 1) { if (allowLists) {
return [...acc, `\`\`\`${val.join('\n')}\`\`\``]; lines.push(source);
} else { } else {
return [...acc, ...val]; 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) => ( return lines.map((line, i) => (
<React.Fragment key={i}> <React.Fragment key={i}>
@ -118,7 +136,7 @@ const MessageMarkdown = React.memo((props) => {
[ [
RemarkDisableTokenizers, RemarkDisableTokenizers,
{ {
block: DISABLED_BLOCK_TOKENS, block: modifiedBlockTokens,
inline: DISABLED_INLINE_TOKENS inline: DISABLED_INLINE_TOKENS
} }
] ]
@ -130,23 +148,26 @@ const MessageMarkdown = React.memo((props) => {
export default function TextContent(props) { export default function TextContent(props) {
const content = props.content; 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-])+([/-])?)+/ /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/
); );
const isGroupLink = const isGroupLink =
group !== null && // matched possible chatroom group !== null && // matched possible chatroom
group[2].length > 2 && // possible ship? group[2].length > 2 && // possible ship?
urbitOb.isValidPatp(group[2]) && // valid patp? 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) { if (isGroupLink) {
const resource = `/ship/${content.text}`; const resource = `/ship/${content.text.trim()}`;
return ( return (
<GroupLink <GroupLink
resource={resource} resource={resource}
api={props.api} api={props.api}
pl='2' pl='2'
my='2'
border='1' border='1'
borderRadius='2' borderRadius='2'
borderColor='washedGray' borderColor='washedGray'
@ -161,7 +182,7 @@ export default function TextContent(props) {
lineHeight={props.lineHeight ? props.lineHeight : '20px'} lineHeight={props.lineHeight ? props.lineHeight : '20px'}
style={{ overflowWrap: 'break-word' }} style={{ overflowWrap: 'break-word' }}
> >
<MessageMarkdown source={content.text} /> <MessageMarkdown source={content.text} allowHeaders={allowHeaders} allowLists={allowLists} />
</Text> </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 React, { ReactElement } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import GlobalApi from '~/logic/api/global'; 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)` const TruncatedBox = styled(Col)`
display: -webkit-box; display: -webkit-box;
@ -31,9 +31,9 @@ const PostContent = (props: PostContentProps): ReactElement => {
textOverflow="ellipsis" textOverflow="ellipsis"
overflow="hidden" overflow="hidden"
> >
<GraphContentWide <GraphContent
transcluded={0} transcluded={0}
post={post} contents={post.contents}
api={api} api={api}
showOurContact showOurContact
/> />