mirror of
https://github.com/urbit/shrub.git
synced 2024-12-20 09:21:42 +03:00
permalinks: support in apps
This commit is contained in:
parent
c7015e2080
commit
f9e7f4602c
@ -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 {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
@ -139,6 +139,8 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
})();
|
})();
|
||||||
}, [groupPath, group]);
|
}, [groupPath, group]);
|
||||||
|
|
||||||
|
console.log(graph);
|
||||||
|
|
||||||
if(!graph) {
|
if(!graph) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
@ -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'>
|
||||||
|
@ -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%'}>
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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
|
||||||
|
@ -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}`);
|
||||||
|
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user