mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 03:14:30 +03:00
permalinks: support in apps
This commit is contained in:
parent
c7015e2080
commit
f9e7f4602c
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -139,6 +139,8 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
})();
|
||||
}, [groupPath, group]);
|
||||
|
||||
console.log(graph);
|
||||
|
||||
if(!graph) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
@ -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'>
|
||||
|
@ -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%'}>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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}`);
|
||||
|
@ -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">
|
||||
@ -158,4 +158,4 @@ class Landscape extends Component<LandscapeProps, Record<string, never>> {
|
||||
|
||||
export default withState(Landscape, [
|
||||
[useHarkState, ['notificationsCount']]
|
||||
]);
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user