diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js index 85a5b5234c..e55be82bbf 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -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 { diff --git a/pkg/interface/src/logic/lib/useQuery.ts b/pkg/interface/src/logic/lib/useQuery.ts index 735060a2da..be45545977 100644 --- a/pkg/interface/src/logic/lib/useQuery.ts +++ b/pkg/interface/src/logic/lib/useQuery.ts @@ -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) { + _.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) => { - const newQuery = new URLSearchParams(search); - _.forIn(q, (value, key) => { - if (!value) { - newQuery.delete(key); - } else { - newQuery.append(key, value); - } - }); - - return newQuery.toString(); + (added: Record) => { + const q = new URLSearchParams(search); + mergeQuery(q, added); + return q.toString(); }, [search] ); + const toQuery = useCallback( + (params: Record, path = pathname) => { + const q = new URLSearchParams(search); + mergeQuery(q, params); + return { + pathname: path, + search: q.toString(), + }; + }, + [search, pathname] + ); + return { query, - appendQuery + appendQuery, + toQuery, }; } diff --git a/pkg/interface/src/logic/state/base.ts b/pkg/interface/src/logic/state/base.ts index 72af05e92b..afc81297bb 100644 --- a/pkg/interface/src/logic/state/base.ts +++ b/pkg/interface/src/logic/state/base.ts @@ -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 = ( @@ -53,12 +53,16 @@ export const createState = >( name: string, properties: Omit, blacklist: string[] = [] -): UseStore => 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 -})); \ No newline at end of file +): UseStore => { + 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)); +} diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index afd718e4a5..cf562dc72e 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -139,6 +139,8 @@ export function ChatResource(props: ChatResourceProps) { })(); }, [groupPath, group]); + console.log(graph); + if(!graph) { return ; } diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 643f43d850..1b736a5e80 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -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 ( { console.log(e)}> Reply - - { copied ? 'Copied' : 'Copy Message Link'} + + {copyDisplay} {isAdmin() || isOwn() ? ( console.log(e)} color='red'> diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index 9e57dc1c47..2b01daeda6 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -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(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 ( diff --git a/pkg/interface/src/views/components/StatelessAsyncButton.tsx b/pkg/interface/src/views/components/StatelessAsyncButton.tsx index 68c27d2263..c4c5bc0c51 100644 --- a/pkg/interface/src/views/components/StatelessAsyncButton.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncButton.tsx @@ -8,11 +8,14 @@ interface AsyncButtonProps { children: ReactNode; name?: string; onClick: (e: React.MouseEvent) => Promise; + /** 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) ? ( - ) : state === 'success' ? ( + ) : state === 'error' ? ( + 'Error' + ) : state === 'success' ? ( 'Done' ) : ( children diff --git a/pkg/interface/src/views/components/UnjoinedResource.tsx b/pkg/interface/src/views/components/UnjoinedResource.tsx index 8e9156a7c0..8d2941a979 100644 --- a/pkg/interface/src/views/components/UnjoinedResource.tsx +++ b/pkg/interface/src/views/components/UnjoinedResource.tsx @@ -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 (
diff --git a/pkg/interface/src/views/landscape/components/Content.js b/pkg/interface/src/views/landscape/components/Content.js index a0f5cbe93c..f6093e8810 100644 --- a/pkg/interface/src/views/landscape/components/Content.js +++ b/pkg/interface/src/views/landscape/components/Content.js @@ -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) => { )} /> + ( (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}`); diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index 0e85f556a1..00ca5a652f 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -136,7 +136,7 @@ class Landscape extends Component> { { const { ship, name } = routeProps.match.params; - const autojoin = ship && name ? `${ship}/${name}` : null; + const autojoin = ship && name ? `${ship}/${name}` : undefined; return ( @@ -158,4 +158,4 @@ class Landscape extends Component> { export default withState(Landscape, [ [useHarkState, ['notificationsCount']] -]); \ No newline at end of file +]);