permalinks: support in apps

This commit is contained in:
Liam Fitzgerald 2021-03-17 14:07:36 +10:00
parent c7015e2080
commit f9e7f4602c
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
11 changed files with 100 additions and 48 deletions

View File

@ -1,6 +1,6 @@
import urbitOb from 'urbit-ob'; import urbitOb from 'urbit-ob';
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 isUrl = (string) => { const isUrl = (string) => {
try { try {

View File

@ -1,30 +1,46 @@
import { useMemo, useCallback } from 'react'; import { useMemo, useCallback } from "react";
import { useLocation } from 'react-router-dom'; import { useLocation } from "react-router-dom";
import _ from 'lodash'; import _ from "lodash";
function mergeQuery(search: URLSearchParams, added: Record<string, string>) {
_.forIn(added, (v, k) => {
if (v) {
search.append(k, v);
} else {
search.delete(k);
}
});
}
export function useQuery() { export function useQuery() {
const { search } = useLocation(); const { search, pathname } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]); const query = useMemo(() => new URLSearchParams(search), [search]);
const appendQuery = useCallback( const appendQuery = useCallback(
(q: Record<string, string>) => { (added: Record<string, string>) => {
const newQuery = new URLSearchParams(search); const q = new URLSearchParams(search);
_.forIn(q, (value, key) => { mergeQuery(q, added);
if (!value) { return q.toString();
newQuery.delete(key);
} else {
newQuery.append(key, value);
}
});
return newQuery.toString();
}, },
[search] [search]
); );
const toQuery = useCallback(
(params: Record<string, string>, path = pathname) => {
const q = new URLSearchParams(search);
mergeQuery(q, params);
return {
pathname: path,
search: q.toString(),
};
},
[search, pathname]
);
return { return {
query, query,
appendQuery appendQuery,
toQuery,
}; };
} }

View File

@ -1,7 +1,7 @@
import produce from "immer"; import produce from "immer";
import { compose } from "lodash/fp"; import { compose } from "lodash/fp";
import create, { State, UseStore } from "zustand"; import create, { State, UseStore } from "zustand";
import { persist } from "zustand/middleware"; import { persist, devtools } from "zustand/middleware";
export const stateSetter = <StateType>( export const stateSetter = <StateType>(
@ -53,12 +53,16 @@ export const createState = <StateType extends BaseState<any>>(
name: string, name: string,
properties: Omit<StateType, 'set'>, properties: Omit<StateType, 'set'>,
blacklist: string[] = [] blacklist: string[] = []
): UseStore<StateType> => create(persist((set, get) => ({ ): UseStore<StateType> => {
// TODO why does this typing break? const storageKey = stateStorageKey(name);
set: fn => stateSetter(fn, set),
...properties return create(devtools(persist((set, get) => ({
}), { // TODO why does this typing break?
blacklist, set: fn => stateSetter(fn, set),
name: stateStorageKey(name), ...properties
version: 1, // TODO version these according to base hash }), {
})); blacklist,
name: storageKey,
version: 1, // TODO version these according to base hash
}), storageKey));
}

View File

@ -139,6 +139,8 @@ export function ChatResource(props: ChatResourceProps) {
})(); })();
}, [groupPath, group]); }, [groupPath, group]);
console.log(graph);
if(!graph) { if(!graph) {
return <Loading />; return <Loading />;
} }

View File

@ -40,6 +40,7 @@ import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import Timestamp from '~/views/components/Timestamp'; import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contact'; import useContactState from '~/logic/state/contact';
import { useIdlingState } from '~/logic/lib/idling'; import { useIdlingState } from '~/logic/lib/idling';
import {useCopy} from '~/logic/lib/useCopy';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -137,14 +138,8 @@ const MessageActionItem = (props) => {
const MessageActions = ({ api, association, history, msg, group }) => { const MessageActions = ({ api, association, history, msg, group }) => {
const isAdmin = () => group.tags.role.admin.has(window.ship); const isAdmin = () => group.tags.role.admin.has(window.ship);
const isOwn = () => msg.author === window.ship; const isOwn = () => msg.author === window.ship;
const [copied, setCopied] = useState(false); const { doCopy, copyDisplay } = useCopy(`web+urbit://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
const copyLink = useCallback(() => {
writeText(`arvo://~graph/graph${association.resource}${msg.index}`);
setCopied(true);;
setTimeout(() => {
setCopied(false)
}, 2000);
}, [setCopied, association, msg]);
return ( return (
<Box <Box
borderRadius={1} borderRadius={1}
@ -200,8 +195,8 @@ const MessageActions = ({ api, association, history, msg, group }) => {
<MessageActionItem onClick={(e) => console.log(e)}> <MessageActionItem onClick={(e) => console.log(e)}>
Reply Reply
</MessageActionItem> </MessageActionItem>
<MessageActionItem onClick={copyLink}> <MessageActionItem onClick={doCopy}>
{ copied ? 'Copied' : 'Copy Message Link'} {copyDisplay}
</MessageActionItem> </MessageActionItem>
{isAdmin() || isOwn() ? ( {isAdmin() || isOwn() ? (
<MessageActionItem onClick={(e) => console.log(e)} color='red'> <MessageActionItem onClick={(e) => console.log(e)} color='red'>

View File

@ -1,4 +1,4 @@
import React, {useEffect, useRef} from 'react'; import React, {useEffect, useRef, useCallback} from 'react';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
@ -13,6 +13,8 @@ import { MentionText } from '~/views/components/MentionText';
import { roleForShip } from '~/logic/lib/group'; import { roleForShip } from '~/logic/lib/group';
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 {usePermalinkForGraph} from '~/logic/lib/permalinks';
import useMetadataState from '~/logic/state/metadata';
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
cursor: pointer; cursor: pointer;
@ -33,6 +35,9 @@ interface CommentItemProps {
export function CommentItem(props: CommentItemProps): ReactElement { export function CommentItem(props: CommentItemProps): ReactElement {
const { ship, name, api, comment, group } = 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); const ref = useRef<HTMLElement | null>(null);
const [, post] = getLatestCommentRevision(comment); const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending; const disabled = props.pending;
@ -82,7 +87,7 @@ export function CommentItem(props: CommentItemProps): ReactElement {
}, []); }, []);
const { copyDisplay, doCopy } = useCopy(`arvo://~graph/graph/ship/${ship}/${name}${comment.post.index}`, 'Copy Link') const { copyDisplay, doCopy } = useCopy(usePermalinkForGraph(association), 'Copy Link');
return ( return (
<Box ref={ref} mb={4} opacity={post?.pending ? '60%' : '100%'}> <Box ref={ref} mb={4} opacity={post?.pending ? '60%' : '100%'}>

View File

@ -8,11 +8,14 @@ interface AsyncButtonProps {
children: ReactNode; children: ReactNode;
name?: string; name?: string;
onClick: (e: React.MouseEvent) => Promise<void>; onClick: (e: React.MouseEvent) => Promise<void>;
/** Manual override */
loading?: boolean;
} }
export function StatelessAsyncButton({ export function StatelessAsyncButton({
children, children,
onClick, onClick,
loading,
name = '', name = '',
disabled = false, disabled = false,
...rest ...rest
@ -29,16 +32,16 @@ export function StatelessAsyncButton({
onClick={handleClick} onClick={handleClick}
{...rest} {...rest}
> >
{state === 'error' ? ( {(state === 'loading' || loading) ? (
'Error'
) : state === 'loading' ? (
<LoadingSpinner <LoadingSpinner
foreground={ foreground={
rest.primary ? 'white' : rest.destructive ? 'red' : 'black' rest.primary ? 'white' : rest.destructive ? 'red' : 'black'
} }
background="gray" background="gray"
/> />
) : state === 'success' ? ( ) : state === 'error' ? (
'Error'
) : state === 'success' ? (
'Done' 'Done'
) : ( ) : (
children children

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Association } from '@urbit/api/metadata'; import { Association } from '@urbit/api/metadata';
import { Box, Text, Button, Col, Center } from '@tlon/indigo-react'; import { Box, Text, Button, Col, Center } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText'; import RichText from '~/views/components/RichText';
@ -11,6 +11,7 @@ import {
} from './StatelessAsyncButton'; } from './StatelessAsyncButton';
import { Graphs } from '@urbit/api'; import { Graphs } from '@urbit/api';
import useGraphState from '~/logic/state/graph'; import useGraphState from '~/logic/state/graph';
import {useQuery} from '~/logic/lib/useQuery';
interface UnjoinedResourceProps { interface UnjoinedResourceProps {
association: Association; association: Association;
@ -31,11 +32,14 @@ function isJoined(path: string) {
export function UnjoinedResource(props: UnjoinedResourceProps) { export function UnjoinedResource(props: UnjoinedResourceProps) {
const { api } = props; const { api } = props;
const history = useHistory(); const history = useHistory();
const { query } = useQuery();
const rid = props.association.resource; const rid = props.association.resource;
const appName = props.association['app-name']; const appName = props.association['app-name'];
const { title, description, module: mod } = props.association.metadata; const { title, description, module: mod } = props.association.metadata;
const graphKeys = useGraphState(state => state.graphKeys); const graphKeys = useGraphState(state => state.graphKeys);
const [loading, setLoading] = useState(false);
const waiter = useWaitForProps({...props, graphKeys }); const waiter = useWaitForProps({...props, graphKeys });
const app = useMemo(() => mod || appName, [props.association]); const app = useMemo(() => mod || appName, [props.association]);
@ -43,7 +47,8 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
const [, , ship, name] = rid.split('/'); const [, , ship, name] = rid.split('/');
await api.graph.joinGraph(ship, name); await api.graph.joinGraph(ship, name);
await waiter(isJoined(rid)); await waiter(isJoined(rid));
history.push(`${props.baseUrl}/resource/${app}${rid}`); const redir = query.get('redir') ?? `${props.baseUrl}/resource/${app}${rid}`;
history.push(redir);
}; };
useEffect(() => { useEffect(() => {
@ -52,6 +57,17 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
} }
}, [props.association, graphKeys]); }, [props.association, graphKeys]);
useEffect(() => {
(async () => {
if(query.has('auto')) {
setLoading(true);
await onJoin();
setLoading(false);
}
})();
}, [query]);
return ( return (
<Center p={6}> <Center p={6}>
<Col <Col
@ -71,6 +87,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
<StatelessAsyncButton <StatelessAsyncButton
name={rid} name={rid}
primary primary
loading={loading}
width="fit-content" width="fit-content"
onClick={onJoin} onClick={onJoin}
> >

View File

@ -11,6 +11,7 @@ import Settings from '~/views/apps/settings/settings';
import ErrorComponent from '~/views/components/Error'; import ErrorComponent from '~/views/components/Error';
import Notifications from '~/views/apps/notifications/notifications'; import Notifications from '~/views/apps/notifications/notifications';
import GraphApp from '../../apps/graph/app'; import GraphApp from '../../apps/graph/app';
import { PermalinkRoutes } from '~/views/apps/permalinks/app';
import { useMigrateSettings } from '~/logic/lib/migrateSettings'; import { useMigrateSettings } from '~/logic/lib/migrateSettings';
@ -89,6 +90,7 @@ export const Content = (props) => {
)} )}
/> />
<GraphApp path="/~graph" {...props} /> <GraphApp path="/~graph" {...props} />
<PermalinkRoutes {...props} />
<Route <Route
render={p => ( render={p => (
<ErrorComponent <ErrorComponent

View File

@ -25,6 +25,7 @@ import { GroupSummary } from './GroupSummary';
import useGroupState from '~/logic/state/group'; import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import {TUTORIAL_GROUP_RESOURCE} from '~/logic/lib/tutorialModal'; import {TUTORIAL_GROUP_RESOURCE} from '~/logic/lib/tutorialModal';
import {useQuery} from '~/logic/lib/useQuery';
const formSchema = Yup.object({ const formSchema = Yup.object({
group: Yup.string() group: Yup.string()
@ -71,7 +72,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
MetadataUpdatePreview | string | null MetadataUpdatePreview | string | null
>(null); >(null);
const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 5000); const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 30000);
const { query } = useQuery();
const onConfirm = useCallback(async (group: string) => { const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/'); const [,,ship,name] = group.split('/');
@ -86,6 +89,11 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|| group in (p.associations?.groups ?? {})); || group in (p.associations?.groups ?? {}));
}); });
if(query.has('redir')) {
const redir = query.get('redir')!;
history.push(redir);
}
if(groups?.[group]?.hidden) { if(groups?.[group]?.hidden) {
const { metadata } = associations.graph[group]; const { metadata } = associations.graph[group];
history.push(`/~landscape/home/resource/${metadata.module}${group}`); history.push(`/~landscape/home/resource/${metadata.module}${group}`);

View File

@ -136,7 +136,7 @@ class Landscape extends Component<LandscapeProps, Record<string, never>> {
<Route path="/~landscape/join/:ship?/:name?" <Route path="/~landscape/join/:ship?/:name?"
render={(routeProps) => { render={(routeProps) => {
const { ship, name } = routeProps.match.params; const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : null; const autojoin = ship && name ? `${ship}/${name}` : undefined;
return ( return (
<Body> <Body>
<Box maxWidth="300px"> <Box maxWidth="300px">