permalinks: add transclusion

This commit is contained in:
Liam Fitzgerald 2021-03-18 10:39:43 +10:00
parent f9e7f4602c
commit ec94f2c5d4
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
18 changed files with 485 additions and 104 deletions

View File

@ -336,15 +336,17 @@ export default class GraphApi extends BaseApi<StoreState> {
});
}
getNode(ship: string, resource: string, index: string) {
const idx = index.split('/').map(numToUd).join('/');
return this.scry<any>(
async getNode(ship: string, resource: string, index: string) {
const idx = index.split('/').map(decToUd).join('/');
const data = await this.scry<any>(
'graph-store',
`/node/${ship}/${resource}${idx}`
).then((node) => {
);
const node = data['graph-update'];
this.store.handleEvent({
data: node
});
data: {
"graph-update-loose": node
}
});
}
}

View File

@ -1,14 +1,22 @@
import { Association, resourceFromPath } from "@urbit/api";
import { Association, resourceFromPath, Group } from "@urbit/api";
import { useGroupForAssoc } from "../state/group";
export function usePermalinkForGraph(assoc: Association, index = "") {
const group = usePermalinkForAssociatedGroup(assoc);
const { ship, name } = resourceFromPath(assoc.resource);
return `${group}/graph/${ship}/${name}${index}`;
const group = useGroupForAssoc(assoc)!;
return getPermalinkForGraph(assoc, group, index);
}
function usePermalinkForAssociatedGroup(assoc: Association) {
const group = useGroupForAssoc(assoc);
export function getPermalinkForGraph(
assoc: Association,
group: Group,
index = ""
) {
const groupLink = getPermalinkForAssociatedGroup(assoc, group);
const { ship, name } = resourceFromPath(assoc.resource);
return `${groupLink}/graph/${ship}/${name}${index}`;
}
function getPermalinkForAssociatedGroup(assoc: Association, group: Group) {
const mod = assoc.metadata.module;
const { ship, name } = resourceFromPath(assoc.group);
if (!group?.hidden) {
@ -19,3 +27,55 @@ function usePermalinkForAssociatedGroup(assoc: Association) {
}
return `web+urbit://mychannel`;
}
type Permalink = GraphPermalink | GroupPermalink;
interface GroupPermalink {
type: "group";
group: string;
link: string;
}
interface GraphPermalink {
type: "graph";
link: string;
graph: string;
group: string;
index: string;
}
function parseGraphPermalink(
link: string,
group: string,
segments: string[]
): GraphPermalink | null {
const [kind, ship, name, ...index] = segments;
if (kind !== "graph") {
return null;
}
const graph = `/ship/${ship}/${name}`;
return {
type: "graph",
link: link.slice(11),
graph,
group,
index: `/${index.join("/")}`,
};
}
export function parsePermalink(url: string): Permalink | null {
const [kind, ...rest] = url.slice(12).split("/");
if (kind === "group") {
const [ship, name, ...graph] = rest;
const group = `/ship/${ship}/${name}`;
if (graph.length > 0) {
return parseGraphPermalink(url, group, graph);
}
return {
type: "group",
group,
link: url.slice(11),
};
}
return null;
}

View File

@ -16,8 +16,27 @@ export const GraphReducer = (json) => {
removeNodes
]);
}
const loose = _.get(json, 'graph-update-loose', false);
if(loose) {
reduceState<GraphState, any>(useGraphState, loose, [addNodesLoose]);
}
};
const addNodesLoose = (json: any, state: GraphState): GraphState => {
const data = _.get(json, 'add-nodes', false);
if(data) {
const { resource: { ship, name }, nodes } = data;
const resource = `${ship}/${name}`;
const indices = _.get(state.looseNodes, [resource], {});
_.forIn(nodes, (node, index) => {
indices[index] = processNode(node);
});
_.set(state.looseNodes, [resource], indices);
}
return state;
}
const keys = (json, state: GraphState): GraphState => {
const data = _.get(json, 'keys', false);
if (data) {
@ -29,9 +48,7 @@ const keys = (json, state: GraphState): GraphState => {
return state;
};
const addGraph = (json, state: GraphState): GraphState => {
const _processNode = (node) => {
const processNode = (node) => {
// is empty
if (!node.children) {
node.children = new BigIntOrderedMap();
@ -46,12 +63,16 @@ const addGraph = (json, state: GraphState): GraphState => {
converted.set(
index,
_processNode(item)
processNode(item)
);
}
node.children = converted;
return node;
};
};
const addGraph = (json, state: GraphState): GraphState => {
const data = _.get(json, 'add-graph', false);
if (data) {
@ -68,7 +89,7 @@ const addGraph = (json, state: GraphState): GraphState => {
let item = data.graph[idx];
let index = bigInt(idx);
let node = _processNode(item);
let node = processNode(item);
state.graphs[resource].set(
index,

View File

@ -1,10 +1,15 @@
import { Graphs, decToUd, numToUd } from "@urbit/api";
import { Graphs, decToUd, numToUd, GraphNode } from "@urbit/api";
import { BaseState, createState } from "./base";
export interface GraphState extends BaseState<GraphState> {
graphs: Graphs;
graphKeys: Set<string>;
looseNodes: {
[graph: string]: {
[index: string]: GraphNode;
}
};
pendingIndices: Record<string, any>;
graphTimesentMap: Record<string, any>;
// getKeys: () => Promise<void>;
@ -21,6 +26,7 @@ export interface GraphState extends BaseState<GraphState> {
const useGraphState = createState<GraphState>('Graph', {
graphs: {},
graphKeys: new Set(),
looseNodes: {},
pendingIndices: {},
graphTimesentMap: {},
// getKeys: async () => {
@ -122,6 +128,6 @@ const useGraphState = createState<GraphState>('Graph', {
// });
// graphReducer(node);
// },
}, ['graphs', 'graphKeys']);
}, ['graphs', 'graphKeys', 'looseNodes']);
export default useGraphState;

View File

@ -20,6 +20,8 @@ import useContactState from '~/logic/state/contact';
import useGraphState from '~/logic/state/graph';
import useGroupState from '~/logic/state/group';
import useHarkState from '~/logic/state/hark';
import {Post} from '@urbit/api';
import {getPermalinkForGraph} from '~/logic/lib/permalinks';
type ChatResourceProps = StoreState & {
association: Association;
@ -95,6 +97,11 @@ export function ChatResource(props: ChatResourceProps) {
const [recipients, setRecipients] = useState([]);
const res = resourceFromPath(groupPath);
const onReply = useCallback((msg: Post) => {
const url = getPermalinkForGraph(props.association, group, msg.index)
const message = `${url}\n~${msg.author} `;
setUnsent(s => ({...s, [props.association.resource]: message }));
}, [props.association, group, setUnsent]);
useEffect(() => {
(async () => {
@ -139,8 +146,6 @@ export function ChatResource(props: ChatResourceProps) {
})();
}, [groupPath, group]);
console.log(graph);
if(!graph) {
return <Loading />;
}
@ -168,6 +173,7 @@ export function ChatResource(props: ChatResourceProps) {
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
group={group}
ship={owner}
onReply={onReply}
station={station}
api={props.api}
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}

View File

@ -41,6 +41,7 @@ import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contact';
import { useIdlingState } from '~/logic/lib/idling';
import {useCopy} from '~/logic/lib/useCopy';
import {PermalinkEmbed} from '../../permalinks/embed';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -135,7 +136,7 @@ const MessageActionItem = (props) => {
);
};
const MessageActions = ({ api, association, history, msg, group }) => {
const MessageActions = ({ api, onReply, association, history, msg, group }) => {
const isAdmin = () => group.tags.role.admin.has(window.ship);
const isOwn = () => msg.author === window.ship;
const { doCopy, copyDisplay } = useCopy(`web+urbit://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
@ -143,7 +144,7 @@ const MessageActions = ({ api, association, history, msg, group }) => {
return (
<Box
borderRadius={1}
background='white'
backgroundColor='white'
border='1px solid'
borderColor='lightGray'
position='absolute'
@ -192,7 +193,7 @@ const MessageActions = ({ api, association, history, msg, group }) => {
Edit Message
</MessageActionItem>
) : null}
<MessageActionItem onClick={(e) => console.log(e)}>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
<MessageActionItem onClick={doCopy}>
@ -220,17 +221,16 @@ const MessageActions = ({ api, association, history, msg, group }) => {
const MessageWrapper = (props) => {
const { hovering, bind } = useHovering();
const showHover = !props.transcluded && hovering && !props.hideHover;
return (
<Box
py='1'
backgroundColor={
hovering && !props.hideHover ? 'washedGray' : 'transparent'
}
backgroundColor={showHover ? 'washedGray' : 'transparent'}
position='relative'
{...bind}
>
{props.children}
{hovering ? <MessageActions {...props} /> : null}
{showHover ? <MessageActions {...props} /> : null}
</Box>
);
};
@ -242,6 +242,7 @@ interface ChatMessageProps {
isLastRead: boolean;
group: Group;
association: Association;
transcluded?: boolean;
className?: string;
isPending: boolean;
style?: unknown;
@ -254,6 +255,7 @@ interface ChatMessageProps {
renderSigil?: boolean;
hideHover?: boolean;
innerRef: (el: HTMLDivElement | null) => void;
onReply?: (msg: Post) => void;
}
class ChatMessage extends Component<ChatMessageProps> {
@ -286,6 +288,8 @@ class ChatMessage extends Component<ChatMessageProps> {
showOurContact,
fontSize,
hideHover
onReply = () => {},
transcluded = false
} = this.props;
let { renderSigil } = this.props;
@ -323,7 +327,12 @@ class ChatMessage extends Component<ChatMessageProps> {
scrollWindow,
highlighted,
fontSize,
<<<<<<< HEAD
hideHover
=======
transcluded,
onReply
>>>>>>> 8c4e175200 (permalinks: add transclusion)
};
const unreadContainerStyle = {
@ -583,6 +592,11 @@ export const Message = ({
case 'code':
return <CodeContent key={i} content={content} />;
case 'url':
if(content.url.startsWith('web+urbit:/')) {
return (
<PermalinkEmbed api={api} link={content.url} />
);
}
return (
<Box
key={i}

View File

@ -42,6 +42,7 @@ type ChatWindowProps = RouteComponentProps<{
station: any;
api: GlobalApi;
scrollTo?: number;
onReply: (msg: Post) => void;
};
interface ChatWindowState {
@ -211,7 +212,8 @@ class ChatWindow extends Component<
graph,
history,
groups,
associations
associations,
onReply
} = this.props;
const { unreadMarkerRef } = this;
const messageProps = {
@ -222,7 +224,8 @@ class ChatWindow extends Component<
history,
api,
groups,
associations
associations,
onReply
};
const msg = graph.get(index)?.post;
@ -278,7 +281,8 @@ class ChatWindow extends Component<
groups,
associations,
showOurContact,
pendingSize
pendingSize,
onReply,
} = this.props;
const unreadMarkerRef = this.unreadMarkerRef;

View File

@ -8,6 +8,7 @@ import { Row, BaseTextArea, Box } from '@tlon/indigo-react';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/lib/codemirror.css';
@ -142,6 +143,10 @@ export default class ChatEditor extends Component {
}
messageChange(editor, data, value) {
if(value.endsWith('/')) {
console.log('showing');
editor.showHint(['test', 'foo']);
}
if (this.state.message !== '' && value == '') {
this.setState({
message: value

View File

@ -105,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
resource={resourcePath}
node={node}
baseUrl={resourceUrl}
association={association}
group={group}
path={resource?.group}
api={api}

View File

@ -11,6 +11,8 @@ import GlobalApi from '~/logic/api/global';
import { Dropdown } from '~/views/components/Dropdown';
import RemoteContent from '~/views/components/RemoteContent';
import useHarkState from '~/logic/state/hark';
import {useCopy} from '~/logic/lib/useCopy';
import {usePermalinkForGraph} from '~/logic/lib/permalinks';
interface LinkItemProps {
node: GraphNode;
@ -70,15 +72,17 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
const ourRole = group ? roleForShip(group, window.ship) : undefined;
const [ship, name] = resource.split('/');
const [locationText, setLocationText] = useState('Copy Link Location');
const copyLocation = () => {
setLocationText('Copied');
writeText(contents[1].url);
setTimeout(() => {
setLocationText('Copy Link Location');
}, 2000);
};
const permalink = usePermalinkForGraph(props.association, node.post.index);
const { doCopy: doCopyLink, copyDisplay: locationText } = useCopy(
contents[1].url,
'Copy Link Location'
);
const { doCopy: doCopyNode, copyDisplay: nodeText } = useCopy(
permalink,
'Copy Node Permalink'
);
const deleteLink = () => {
if (confirm('Are you sure you want to delete this link?')) {
@ -173,8 +177,12 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
options={
<Col backgroundColor="white" border={1} borderRadius={1} borderColor="lightGray">
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="black" onClick={copyLocation}>{locationText}</Action>
<Action bg="white" m={1} color="black" onClick={doCopyLink}>{locationText}</Action>
</Row>
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="black" onClick={doCopyNode}>{nodeText}</Action>
</Row>
{(ourRole === 'admin' || node.post.author === window.ship) &&
<Row alignItems="center" p={1}>
<Action bg="white" m={1} color="red" destructive onClick={deleteLink}>Delete Link</Action>

View File

@ -66,13 +66,11 @@ const GraphUrl = ({ url, title }) => (
</Box>
);
const GraphNodeContent = ({
export const GraphNodeContent = ({
group,
post,
mod,
description,
index,
remoteContentPolicy
}) => {
const { contents } = post;
const idx = index.slice(1).split('/');

View File

@ -0,0 +1,127 @@
import React from "react";
import { Anchor, Icon, Box, Row, Col, Text } from "@tlon/indigo-react";
import ChatMessage from "../chat/components/ChatMessage";
import { Association, GraphNode } from "@urbit/api";
import { useGroupForAssoc } from "~/logic/state/group";
import { MentionText } from "~/views/components/MentionText";
import Author from "~/views/components/Author";
import { NoteContent } from "../publish/components/Note";
import bigInt from "big-integer";
import { getSnippet } from "~/logic/lib/publish";
import { NotePreviewContent } from "../publish/components/NotePreview";
function TranscludedLinkNode(props: { node: GraphNode; assoc: Association }) {
const { node, assoc } = props;
const idx = node.post.index.slice(1).split("/");
switch (idx.length) {
case 1:
const [{ text }, { url }] = node.post.contents;
return (
<Box borderRadius="2" p="2" bg="scales.black05">
<Anchor underline={false} target="_blank" color="black" href={url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
{text}
</Anchor>
</Box>
);
case 2:
return <TranscludedComment node={node} assoc={assoc} />;
default:
return null;
}
}
function TranscludedComment(props: { node: GraphNode; assoc: Association }) {
const { assoc, node } = props;
const group = useGroupForAssoc(assoc)!;
const comment = node.children?.peekLargest()![1]!;
return (
<Col>
<Author
p="2"
showImage
ship={comment.post.author}
date={comment.post?.["time-sent"]}
group={group}
/>
<Box p="2">
<MentionText content={comment.post.contents} group={group} />;
</Box>
</Col>
);
}
function TranscludedPublishNode(props: {
node: GraphNode;
assoc: Association;
}) {
const { node, assoc } = props;
const group = useGroupForAssoc(assoc)!;
const idx = node.post.index.slice(1).split("/");
switch (idx.length) {
case 1:
const post = node.children
?.get(bigInt.one)
?.children?.peekLargest()?.[1]!;
return (
<Col>
<Author
p="2"
showImage
ship={post.post.author}
date={post.post?.["time-sent"]}
group={group}
/>
<Text px="2" fontSize="2" fontWeight="medium">
{post.post.contents[0]?.text}
</Text>
<Box p="2">
<NotePreviewContent
snippet={getSnippet(post?.post.contents[1]?.text)}
/>
</Box>
</Col>
);
case 3:
return <TranscludedComment node={node} assoc={assoc} />;
default:
return null;
}
}
export function TranscludedNode(props: {
assoc: Association;
node: GraphNode;
}) {
const { node, assoc } = props;
const group = useGroupForAssoc(assoc)!;
switch (assoc.metadata.module) {
case "chat":
return (
<Row width="100%" flexShrink={0} flexGrow={1} flexWrap="wrap">
<ChatMessage
renderSigil
transcluded
containerClass="items-top cf hide-child"
association={assoc}
group={group}
groups={{}}
msg={node.post}
fontSize="0"
ml="0"
mr="0"
pt="2"
/>
</Row>
);
case "publish":
return <TranscludedPublishNode {...props} />;
case "link":
return <TranscludedLinkNode {...props} />;
default:
return null;
}
}

View File

@ -85,7 +85,8 @@ function GroupRoutes(props: { group: string; url: string }) {
if(!association) {
return null;
}
if(!graphKeys.has(`${ship}/${name}`)) {
console.log(graphKeys);
if(!graphKeys.has(`${ship.slice(1)}/${name}`)) {
if(graphKeys.size > 0) {
return <Redirect
to={toQuery(

View File

@ -0,0 +1,117 @@
import React, { useCallback, useEffect, useState } from "react";
import {
parsePermalink,
GraphPermalink as IGraphPermalink,
} from "~/logic/lib/permalinks";
import { Box, Text, BaseAnchor, Row, Icon, Col } from "@tlon/indigo-react";
import { GroupLink } from "~/views/components/GroupLink";
import GlobalApi from "~/logic/api/global";
import { getModuleIcon } from "~/logic/lib/util";
import useMetadataState from "~/logic/state/metadata";
import { Association, resourceFromPath } from "@urbit/api";
import { Link } from "react-router-dom";
import useGraphState from "~/logic/state/graph";
import { GraphNodeContent } from "../notifications/graph";
import { TranscludedNode } from "./TranscludedNode";
function GroupPermalink(props: { group: string; api: GlobalApi }) {
const { group, api } = props;
return (
<GroupLink
resource={group}
api={api}
pl="2"
border="1"
borderRadius="2"
borderColor="washedGray"
/>
);
}
function GraphPermalink(props: IGraphPermalink & { api: GlobalApi }) {
const { link, graph, group, index, api } = props;
const { ship, name } = resourceFromPath(graph);
const node = useGraphState(
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
graph,
index,
])
);
const [errored, setErrored] = useState(false);
const association = useMetadataState(
useCallback((s) => s.associations.graph[graph] as Association | null, [
graph,
])
);
useEffect(() => {
(async () => {
try {
await api.graph.getNode(ship, name, index);
} catch (e) {
console.log(e);
setErrored(true);
}
})();
}, [graph, index]);
const showTransclusion = !!(association && node);
const rowTransclusionStyle = showTransclusion
? {
borderTop: "1",
borderTopColor: "washedGray",
my: "1",
}
: {};
return (
<Link to={`/perma${link}`}>
<Col
my="1"
bg="white"
border="1"
borderColor="lightGray"
borderRadius="2"
>
{association && node && (
<Box p="2">
<TranscludedNode node={node} assoc={association} />
</Box>
)}
<Row
{...rowTransclusionStyle}
alignItems="center"
height="32px"
gapX="2"
width="100%"
px="2"
>
<Icon
icon={
association
? (getModuleIcon(association.metadata.module) as any)
: "Groups"
}
/>
<Text lineHeight="20px" mono={!association}>
{association?.metadata.title ?? graph.slice(6)}
</Text>
</Row>
</Col>
</Link>
);
}
export function PermalinkEmbed(props: { link: string; api: GlobalApi }) {
const permalink = parsePermalink(props.link);
if (!permalink) {
return <BaseAnchor href={props.link}>{props.link}</BaseAnchor>;
}
switch (permalink.type) {
case "group":
return <GroupPermalink group={permalink.group} api={props.api} />;
case "graph":
return <GraphPermalink {...permalink} api={props.api} />;
}
}

View File

@ -25,19 +25,29 @@ interface NoteProps {
group: Group;
}
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, ship, book, api, rootUrl, baseUrl, group } = props;
const editCommentId = props.match.params.commentId;
const renderers = {
const renderers = {
link: ({ href, children }) => {
return (
<Anchor display="inline" target="_blank" href={href}>{children}</Anchor>
)
}
};
};
export function NoteContent({ body }) {
return (
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
</Box>
);
}
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, ship, book, api, rootUrl, baseUrl, group } = props;
const editCommentId = props.match.params.commentId;
const deletePost = async () => {
setDeleting(true);
@ -122,9 +132,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Text ml={1}>{adminLinks}</Text>
</Row>
</Col>
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
</Box>
<NoteContent body={body} />
<NoteNavigation
notebook={notebook}
noteId={noteId}

View File

@ -27,6 +27,27 @@ const WrappedBox = styled(Box)`
overflow-wrap: break-word;
`;
export function NotePreviewContent({ snippet }) {
return (
<ReactMarkdown
unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}
renderers={{
image: props => (
<Box
backgroundImage={`url(${props.src})`}
style={{ backgroundSize: 'cover',
backgroundPosition: "center" }}
>
<Image src={props.src} opacity="0" maxHeight="300px"/>
</Box>
)
}}
source={snippet}
/>
);
}
export function NotePreview(props: NotePreviewProps) {
const { node, group } = props;
const { post } = node;
@ -67,22 +88,7 @@ export function NotePreview(props: NotePreviewProps) {
<WrappedBox mb={2}><Text bold>{title}</Text></WrappedBox>
<WrappedBox>
<Text fontSize='14px' lineHeight='tall'>
<ReactMarkdown
unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}
renderers={{
image: props => (
<Box
backgroundImage={`url(${props.src})`}
style={{ backgroundSize: 'cover',
backgroundPosition: "center" }}
>
<Image src={props.src} opacity="0" maxHeight="300px"/>
</Box>
)
}}
source={snippet}
/>
<NotePreviewContent snippet={snippet} />
</Text>
</WrappedBox>
</Col>

View File

@ -13,6 +13,7 @@ import OverlaySigil from './OverlaySigil';
import { Sigil } from '~/logic/lib/sigil';
import Timestamp from './Timestamp';
import useContactState from '~/logic/state/contact';
import {PropFunc} from '~/types';
interface AuthorProps {
ship: string;
@ -24,8 +25,8 @@ interface AuthorProps {
}
// eslint-disable-next-line max-lines-per-function
export default function Author(props: AuthorProps): ReactElement {
const { ship = '', date, showImage, group } = props;
export default function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
const { ship = '', date, showImage, children, unread, group, ...rest } = props;
const history = useHistory();
const osDark = useLocalState((state) => state.dark);
@ -65,7 +66,7 @@ export default function Author(props: AuthorProps): ReactElement {
);
return (
<Row alignItems='center' width='auto'>
<Row {...rest} alignItems='center' width='auto'>
<Box
onClick={() => toggleOverlay()}
height={16}
@ -95,8 +96,8 @@ export default function Author(props: AuthorProps): ReactElement {
>
{name}
</Box>
<Timestamp stamp={stamp} fontSize={1} time={false} ml={2} color={props.unread ? 'blue' : 'gray'} />
{props.children}
<Timestamp stamp={stamp} fontSize={1} time={false} ml={2} color={unread ? 'blue' : 'gray'} />
{children}
</Row>
);
}

View File

@ -79,15 +79,11 @@ export function CommentItem(props: CommentItemProps): ReactElement {
}, [ref, props.highlighted]);
const history = useHistory();
useEffect(() => {
return history.listen((location, action) => {
console.log(location);
console.log(action);
});
}, []);
const { copyDisplay, doCopy } = useCopy(usePermalinkForGraph(association), 'Copy Link');
const { copyDisplay, doCopy } = useCopy(
usePermalinkForGraph(association, post.index.split('/').slice(0, -1).join('/')),
'Copy Link'
);
return (
<Box ref={ref} mb={4} opacity={post?.pending ? '60%' : '100%'}>