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';
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) => {
try {

View File

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

View File

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

View File

@ -40,6 +40,7 @@ import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contact';
import { useIdlingState } from '~/logic/lib/idling';
import {useCopy} from '~/logic/lib/useCopy';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -137,14 +138,8 @@ const MessageActionItem = (props) => {
const MessageActions = ({ api, association, history, msg, group }) => {
const isAdmin = () => group.tags.role.admin.has(window.ship);
const isOwn = () => msg.author === window.ship;
const [copied, setCopied] = useState(false);
const copyLink = useCallback(() => {
writeText(`arvo://~graph/graph${association.resource}${msg.index}`);
setCopied(true);;
setTimeout(() => {
setCopied(false)
}, 2000);
}, [setCopied, association, msg]);
const { doCopy, copyDisplay } = useCopy(`web+urbit://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
return (
<Box
borderRadius={1}
@ -200,8 +195,8 @@ const MessageActions = ({ api, association, history, msg, group }) => {
<MessageActionItem onClick={(e) => console.log(e)}>
Reply
</MessageActionItem>
<MessageActionItem onClick={copyLink}>
{ copied ? 'Copied' : 'Copy Message Link'}
<MessageActionItem onClick={doCopy}>
{copyDisplay}
</MessageActionItem>
{isAdmin() || isOwn() ? (
<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 styled from 'styled-components';
@ -13,6 +13,8 @@ import { MentionText } from '~/views/components/MentionText';
import { roleForShip } from '~/logic/lib/group';
import { getLatestCommentRevision } from '~/logic/lib/publish';
import {useCopy} from '~/logic/lib/useCopy';
import {usePermalinkForGraph} from '~/logic/lib/permalinks';
import useMetadataState from '~/logic/state/metadata';
const ClickBox = styled(Box)`
cursor: pointer;
@ -33,6 +35,9 @@ interface CommentItemProps {
export function CommentItem(props: CommentItemProps): ReactElement {
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 [, post] = getLatestCommentRevision(comment);
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 (
<Box ref={ref} mb={4} opacity={post?.pending ? '60%' : '100%'}>

View File

@ -8,11 +8,14 @@ interface AsyncButtonProps {
children: ReactNode;
name?: string;
onClick: (e: React.MouseEvent) => Promise<void>;
/** Manual override */
loading?: boolean;
}
export function StatelessAsyncButton({
children,
onClick,
loading,
name = '',
disabled = false,
...rest
@ -29,16 +32,16 @@ export function StatelessAsyncButton({
onClick={handleClick}
{...rest}
>
{state === 'error' ? (
'Error'
) : state === 'loading' ? (
{(state === 'loading' || loading) ? (
<LoadingSpinner
foreground={
rest.primary ? 'white' : rest.destructive ? 'red' : 'black'
}
background="gray"
/>
) : state === 'success' ? (
) : state === 'error' ? (
'Error'
) : state === 'success' ? (
'Done'
) : (
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 { Box, Text, Button, Col, Center } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
@ -11,6 +11,7 @@ import {
} from './StatelessAsyncButton';
import { Graphs } from '@urbit/api';
import useGraphState from '~/logic/state/graph';
import {useQuery} from '~/logic/lib/useQuery';
interface UnjoinedResourceProps {
association: Association;
@ -31,11 +32,14 @@ function isJoined(path: string) {
export function UnjoinedResource(props: UnjoinedResourceProps) {
const { api } = props;
const history = useHistory();
const { query } = useQuery();
const rid = props.association.resource;
const appName = props.association['app-name'];
const { title, description, module: mod } = props.association.metadata;
const graphKeys = useGraphState(state => state.graphKeys);
const [loading, setLoading] = useState(false);
const waiter = useWaitForProps({...props, graphKeys });
const app = useMemo(() => mod || appName, [props.association]);
@ -43,7 +47,8 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
const [, , ship, name] = rid.split('/');
await api.graph.joinGraph(ship, name);
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(() => {
@ -52,6 +57,17 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
}
}, [props.association, graphKeys]);
useEffect(() => {
(async () => {
if(query.has('auto')) {
setLoading(true);
await onJoin();
setLoading(false);
}
})();
}, [query]);
return (
<Center p={6}>
<Col
@ -71,6 +87,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
<StatelessAsyncButton
name={rid}
primary
loading={loading}
width="fit-content"
onClick={onJoin}
>

View File

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

View File

@ -25,6 +25,7 @@ import { GroupSummary } from './GroupSummary';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
import {TUTORIAL_GROUP_RESOURCE} from '~/logic/lib/tutorialModal';
import {useQuery} from '~/logic/lib/useQuery';
const formSchema = Yup.object({
group: Yup.string()
@ -71,7 +72,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
MetadataUpdatePreview | string | 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 [,,ship,name] = group.split('/');
@ -86,6 +89,11 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|| group in (p.associations?.groups ?? {}));
});
if(query.has('redir')) {
const redir = query.get('redir')!;
history.push(redir);
}
if(groups?.[group]?.hidden) {
const { metadata } = associations.graph[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?"
render={(routeProps) => {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : null;
const autojoin = ship && name ? `${ship}/${name}` : undefined;
return (
<Body>
<Box maxWidth="300px">