mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 01:52:42 +03:00
permalinks: add transclusion
This commit is contained in:
parent
f9e7f4602c
commit
ec94f2c5d4
@ -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) => {
|
||||
this.store.handleEvent({
|
||||
data: node
|
||||
});
|
||||
);
|
||||
const node = data['graph-update'];
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
"graph-update-loose": node
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,29 +48,31 @@ const keys = (json, state: GraphState): GraphState => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const processNode = (node) => {
|
||||
// is empty
|
||||
if (!node.children) {
|
||||
node.children = new BigIntOrderedMap();
|
||||
return node;
|
||||
}
|
||||
|
||||
// is graph
|
||||
let converted = new BigIntOrderedMap();
|
||||
for (let idx in node.children) {
|
||||
let item = node.children[idx];
|
||||
let index = bigInt(idx);
|
||||
|
||||
converted.set(
|
||||
index,
|
||||
processNode(item)
|
||||
);
|
||||
}
|
||||
node.children = converted;
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
const addGraph = (json, state: GraphState): GraphState => {
|
||||
|
||||
const _processNode = (node) => {
|
||||
// is empty
|
||||
if (!node.children) {
|
||||
node.children = new BigIntOrderedMap();
|
||||
return node;
|
||||
}
|
||||
|
||||
// is graph
|
||||
let converted = new BigIntOrderedMap();
|
||||
for (let idx in node.children) {
|
||||
let item = node.children[idx];
|
||||
let index = bigInt(idx);
|
||||
|
||||
converted.set(
|
||||
index,
|
||||
_processNode(item)
|
||||
);
|
||||
}
|
||||
node.children = converted;
|
||||
return node;
|
||||
};
|
||||
|
||||
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,
|
||||
|
@ -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;
|
||||
export default useGraphState;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -105,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
resource={resourcePath}
|
||||
node={node}
|
||||
baseUrl={resourceUrl}
|
||||
association={association}
|
||||
group={group}
|
||||
path={resource?.group}
|
||||
api={api}
|
||||
|
@ -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,16 +72,18 @@ 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?')) {
|
||||
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
|
||||
@ -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>
|
||||
|
@ -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('/');
|
||||
|
127
pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx
Normal file
127
pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx
Normal 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;
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
117
pkg/interface/src/views/apps/permalinks/embed.tsx
Normal file
117
pkg/interface/src/views/apps/permalinks/embed.tsx
Normal 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} />;
|
||||
}
|
||||
}
|
@ -25,20 +25,30 @@ interface NoteProps {
|
||||
group: Group;
|
||||
}
|
||||
|
||||
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 renderers = {
|
||||
link: ({ href, children }) => {
|
||||
return (
|
||||
<Anchor display="inline" target="_blank" href={href}>{children}</Anchor>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
const deletePost = async () => {
|
||||
setDeleting(true);
|
||||
const indices = [note.post.index];
|
||||
@ -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}
|
||||
|
@ -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;
|
||||
@ -66,23 +87,8 @@ 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}
|
||||
/>
|
||||
<Text fontSize='14px' lineHeight='tall'>
|
||||
<NotePreviewContent snippet={snippet} />
|
||||
</Text>
|
||||
</WrappedBox>
|
||||
</Col>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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%'}>
|
||||
|
Loading…
Reference in New Issue
Block a user