diff --git a/apps/tlon-web/src/app.tsx b/apps/tlon-web/src/app.tsx
index 5b5de81bab..86360d73c1 100644
--- a/apps/tlon-web/src/app.tsx
+++ b/apps/tlon-web/src/app.tsx
@@ -156,15 +156,16 @@ const SuspendedDiaryAddNote = (
);
interface RoutesProps {
- state: { backgroundLocation?: Location } | null;
- location: Location;
isMobile: boolean;
isSmall: boolean;
}
-function GroupsRoutes({ state, location, isMobile, isSmall }: RoutesProps) {
+const GroupsRoutes = React.memo(({ isMobile, isSmall }: RoutesProps) => {
const groupsTitle = 'Tlon';
const loaded = useSettingsLoaded();
+ const location = useLocation();
+
+ const state = location.state as { backgroundLocation?: Location } | null;
useEffect(() => {
if (loaded) {
@@ -558,7 +559,7 @@ function GroupsRoutes({ state, location, isMobile, isSmall }: RoutesProps) {
) : null}
>
);
-}
+});
function authRedirect() {
document.location = `${document.location.protocol}//${document.location.host}`;
@@ -613,7 +614,6 @@ function App() {
useNativeBridge();
const navigate = useNavigate();
const handleError = useErrorHandler();
- const location = useLocation();
const isMobile = useIsMobile();
const isSmall = useMedia('(max-width: 1023px)');
@@ -637,20 +637,13 @@ function App() {
})();
}, [handleError]);
- const state = location.state as { backgroundLocation?: Location } | null;
-
return (
-
+
diff --git a/apps/tlon-web/src/chat/ChatChannel.tsx b/apps/tlon-web/src/chat/ChatChannel.tsx
index 1087bd3d0e..b34cf7a62e 100644
--- a/apps/tlon-web/src/chat/ChatChannel.tsx
+++ b/apps/tlon-web/src/chat/ChatChannel.tsx
@@ -30,7 +30,7 @@ import { useRouteGroup } from '@/state/groups/groups';
import ChatThread from './ChatThread/ChatThread';
-function ChatChannel({ title }: ViewProps) {
+const ChatChannel = React.memo(({ title }: ViewProps) => {
const { isChatInputFocused } = useChatInputFocus();
// TODO: We need to reroute users who can't read the channel
// const navigate = useNavigate();
@@ -195,6 +195,6 @@ function ChatChannel({ title }: ViewProps) {
>
);
-}
+});
export default ChatChannel;
diff --git a/apps/tlon-web/src/chat/ChatInput/ChatInput.tsx b/apps/tlon-web/src/chat/ChatInput/ChatInput.tsx
index 0da270af8e..db711e2dcd 100644
--- a/apps/tlon-web/src/chat/ChatInput/ChatInput.tsx
+++ b/apps/tlon-web/src/chat/ChatInput/ChatInput.tsx
@@ -185,6 +185,7 @@ export default function ChatInput({
const myLastMessage = useMyLastMessage(whom, replying);
const lastMessageId = myLastMessage ? myLastMessage.seal.id : '';
const lastMessageIdRef = useRef(lastMessageId);
+ const isReplyingRef = useRef(!!replying);
useEffect(() => {
if (lastMessageId && lastMessageId !== lastMessageIdRef.current) {
@@ -192,6 +193,13 @@ export default function ChatInput({
}
}, [lastMessageId]);
+ useEffect(() => {
+ if (isReplyingRef.current && !replying) {
+ isReplyingRef.current = false;
+ }
+ isReplyingRef.current = !!replying;
+ }, [replying]);
+
const handleUnblockClick = useCallback(() => {
unblockShip({
ship: whom,
@@ -400,9 +408,11 @@ export default function ChatInput({
!isDmOrMultiDM
) {
setSearchParams(
- {
- edit: lastMessageIdRef.current,
- },
+ isReplyingRef.current
+ ? { editReply: lastMessageIdRef.current }
+ : {
+ edit: lastMessageIdRef.current,
+ },
{ replace: true }
);
editor.commands.blur();
diff --git a/apps/tlon-web/src/chat/ChatSearch/ChatSearch.tsx b/apps/tlon-web/src/chat/ChatSearch/ChatSearch.tsx
index 7cf05d498e..4b90f7869a 100644
--- a/apps/tlon-web/src/chat/ChatSearch/ChatSearch.tsx
+++ b/apps/tlon-web/src/chat/ChatSearch/ChatSearch.tsx
@@ -1,15 +1,13 @@
-import * as Dialog from '@radix-ui/react-dialog';
import { ChatMap } from '@tloncorp/shared/dist/urbit/channel';
import cn from 'classnames';
-import React, { PropsWithChildren, useCallback } from 'react';
+import React, { PropsWithChildren, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router';
import { Link } from 'react-router-dom';
import { VirtuosoHandle } from 'react-virtuoso';
+import { useOnClickOutside } from 'usehooks-ts';
-import useActiveTab from '@/components/Sidebar/util';
import { useSafeAreaInsets } from '@/logic/native';
import useMedia, { useIsMobile } from '@/logic/useMedia';
-import { disableDefault } from '@/logic/utils';
import ChatSearchResults from './ChatSearchResults';
import SearchBar from './SearchBar';
@@ -45,8 +43,8 @@ export default function ChatSearch({
const navigate = useNavigate();
const isMobile = useIsMobile();
const isSmall = useMedia('(min-width: 768px) and (max-width: 1099px)');
- const activeTab = useActiveTab();
const safeAreaInsets = useSafeAreaInsets();
+ const containerRef = useRef(null);
const scrollerRef = React.useRef(null);
const { selected, rawInput, onChange, onKeyDown } = useChatSearchInput({
root,
@@ -68,23 +66,7 @@ export default function ChatSearch({
}, []),
});
- const preventClose = useCallback((e: Event) => {
- const target = e.target as HTMLElement;
- const hasNavAncestor = target.id === 'search' || target.closest('#search');
-
- if (hasNavAncestor) {
- e.preventDefault();
- }
- }, []);
-
- const onOpenChange = useCallback(
- (open: boolean) => {
- if (!open) {
- navigate(root);
- }
- },
- [navigate, root]
- );
+ useOnClickOutside(containerRef, () => navigate(root));
return (
{!isMobile && !isSmall ? children : null}
@@ -112,40 +95,30 @@ export default function ChatSearch({
isSmall={isSmall}
/>
-
-
+
-
+
+
+
{!isSmall && (
diff --git a/apps/tlon-web/src/chat/ChatThread/ChatThread.tsx b/apps/tlon-web/src/chat/ChatThread/ChatThread.tsx
index e3947e82c3..287d34ebb4 100644
--- a/apps/tlon-web/src/chat/ChatThread/ChatThread.tsx
+++ b/apps/tlon-web/src/chat/ChatThread/ChatThread.tsx
@@ -21,6 +21,7 @@ import { useDragAndDrop } from '@/logic/DragAndDropContext';
import { useChannelCompatibility, useChannelFlag } from '@/logic/channel';
import { useBottomPadding } from '@/logic/position';
import { useIsScrolling } from '@/logic/scroll';
+import useIsEditingMessage from '@/logic/useIsEditingMessage';
import useMedia, { useIsMobile } from '@/logic/useMedia';
import {
useAddReplyMutation,
@@ -48,6 +49,7 @@ export default function ChatThread() {
}>();
const isMobile = useIsMobile();
const { isChatInputFocused } = useChatInputFocus();
+ const isEditing = useIsEditingMessage();
const scrollerRef = useRef(null);
const flag = useChannelFlag()!;
const nest = `chat/${flag}`;
@@ -259,7 +261,7 @@ export default function ChatThread() {
searchParams.get('msg') || searchParams.get('edit') || idTime,
- [searchParams, idTime]
- );
- const nest = `chat/${whom}`;
- const {
- posts: messages,
- hasNextPage,
- hasPreviousPage,
- fetchPreviousPage,
- refetch,
- remove,
- fetchNextPage,
- isLoading,
- isFetching,
- isFetchingNextPage,
- isFetchingPreviousPage,
- } = useInfinitePosts(nest, scrollToId);
- const { mutate: markRead } = useMarkReadMutation();
- const scrollerRef = useRef
(null);
- const readTimeout = useChatInfo(whom).unread?.readTimeout;
- const clearOnNavRef = useRef({ readTimeout, nest, whom, markRead });
- const { compatible } = useChannelCompatibility(nest);
- const navigate = useNavigate();
- const latestMessageIndex = messages.length - 1;
- const scrollToIndex = useMemo(
- () =>
- scrollToId
- ? messages.findIndex((m) => m[0].toString() === scrollToId)
- : latestMessageIndex,
- [scrollToId, messages, latestMessageIndex]
- );
- const msgIdTimeInMessages = useMemo(
- () =>
- scrollToId
- ? messages.findIndex((m) => m[0].toString() === scrollToId) !== -1
- : false,
- [scrollToId, messages]
- );
- const latestIsMoreThan30NewerThanScrollTo = useMemo(
- () =>
- scrollToIndex !== latestMessageIndex &&
- latestMessageIndex - scrollToIndex > 30,
- [scrollToIndex, latestMessageIndex]
- );
-
- const goToLatest = useCallback(async () => {
- if (idTime) {
- navigate(root);
- } else {
- setSearchParams({});
- }
- if (hasPreviousPage) {
- // wait until next tick to avoid the race condition where refetch
- // happens before navigation completes and clears scrollToId
- // TODO: is there a better way to handle this?
- setTimeout(() => {
- remove();
- refetch();
- }, 0);
- } else {
- scrollerRef.current?.scrollToIndex({ index: 'LAST', align: 'end' });
- }
- }, [
- setSearchParams,
- remove,
- refetch,
- hasPreviousPage,
- scrollerRef,
- idTime,
- navigate,
+const ChatWindow = React.memo(
+ ({
+ whom,
root,
- ]);
-
- useEffect(() => {
- useChatStore.getState().setCurrent(whom);
-
- return () => {
- useChatStore.getState().setCurrent('');
- };
- }, [whom]);
-
- const onAtBottom = useCallback(() => {
- const { bottom, delayedRead } = useChatStore.getState();
- bottom(true);
- delayedRead(whom, () => markRead({ nest }));
- if (hasPreviousPage && !isFetching) {
- log('fetching previous page');
- fetchPreviousPage();
- }
- }, [nest, whom, markRead, fetchPreviousPage, hasPreviousPage, isFetching]);
+ prefixedElement,
+ scrollElementRef,
+ isScrolling,
+ }: ChatWindowProps) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const { idTime } = useParams();
+ const scrollToId = useMemo(
+ () => searchParams.get('msg') || searchParams.get('edit') || idTime,
+ [searchParams, idTime]
+ );
+ const nest = `chat/${whom}`;
+ const {
+ posts: messages,
+ hasNextPage,
+ hasPreviousPage,
+ fetchPreviousPage,
+ refetch,
+ remove,
+ fetchNextPage,
+ isLoading,
+ isFetching,
+ isFetchingNextPage,
+ isFetchingPreviousPage,
+ } = useInfinitePosts(nest, scrollToId);
+ const { mutate: markRead } = useMarkReadMutation();
+ const scrollerRef = useRef(null);
+ const readTimeout = useChatInfo(whom).unread?.readTimeout;
+ const clearOnNavRef = useRef({ readTimeout, nest, whom, markRead });
+ const { compatible } = useChannelCompatibility(nest);
+ const navigate = useNavigate();
+ const latestMessageIndex = messages.length - 1;
+ const scrollToIndex = useMemo(
+ () =>
+ scrollToId
+ ? messages.findIndex((m) => m[0].toString() === scrollToId)
+ : latestMessageIndex,
+ [scrollToId, messages, latestMessageIndex]
+ );
+ const msgIdTimeInMessages = useMemo(
+ () =>
+ scrollToId
+ ? messages.findIndex((m) => m[0].toString() === scrollToId) !== -1
+ : false,
+ [scrollToId, messages]
+ );
+ const latestIsMoreThan30NewerThanScrollTo = useMemo(
+ () =>
+ scrollToIndex !== latestMessageIndex &&
+ latestMessageIndex - scrollToIndex > 30,
+ [scrollToIndex, latestMessageIndex]
+ );
- const onAtTop = useCallback(() => {
- if (hasNextPage && !isFetching) {
- log('fetching next page');
- fetchNextPage();
- }
- }, [fetchNextPage, hasNextPage, isFetching]);
-
- // read the messages once navigated away
- useEffect(() => {
- clearOnNavRef.current = { readTimeout, nest, whom, markRead };
- }, [readTimeout, nest, whom, markRead]);
-
- useEffect(
- () => () => {
- const curr = clearOnNavRef.current;
- if (curr.readTimeout !== undefined && curr.readTimeout !== 0) {
- useChatStore.getState().read(curr.whom);
- curr.markRead({ nest: curr.nest });
+ const goToLatest = useCallback(async () => {
+ if (idTime) {
+ navigate(root);
+ } else {
+ setSearchParams({});
}
- },
- []
- );
-
- useEffect(() => {
- const doRefetch = async () => {
- remove();
- await refetch();
- };
-
- // If we have a scrollTo, we have a next page, and the scrollTo message is
- // not in our current set of messages, that means we're scrolling to a
- // message that's not yet cached. So, we need to refetch (which would fetch
- // messages around the scrollTo time), then scroll to the message.
- if (scrollToId && hasNextPage && !msgIdTimeInMessages) {
- doRefetch();
- }
- }, [scrollToId, hasNextPage, remove, refetch, msgIdTimeInMessages]);
+ if (hasPreviousPage) {
+ // wait until next tick to avoid the race condition where refetch
+ // happens before navigation completes and clears scrollToId
+ // TODO: is there a better way to handle this?
+ setTimeout(() => {
+ remove();
+ refetch();
+ }, 0);
+ } else {
+ scrollerRef.current?.scrollToIndex({ index: 'LAST', align: 'end' });
+ }
+ }, [
+ setSearchParams,
+ remove,
+ refetch,
+ hasPreviousPage,
+ scrollerRef,
+ idTime,
+ navigate,
+ root,
+ ]);
+
+ useEffect(() => {
+ useChatStore.getState().setCurrent(whom);
+
+ return () => {
+ useChatStore.getState().setCurrent('');
+ };
+ }, [whom]);
+
+ const onAtBottom = useCallback(() => {
+ const { bottom, delayedRead } = useChatStore.getState();
+ bottom(true);
+ delayedRead(whom, () => markRead({ nest }));
+ if (hasPreviousPage && !isFetching) {
+ log('fetching previous page');
+ fetchPreviousPage();
+ }
+ }, [nest, whom, markRead, fetchPreviousPage, hasPreviousPage, isFetching]);
- if (isLoading) {
- return (
-
-
-
+ const onAtTop = useCallback(() => {
+ if (hasNextPage && !isFetching) {
+ log('fetching next page');
+ fetchNextPage();
+ }
+ }, [fetchNextPage, hasNextPage, isFetching]);
+
+ // read the messages once navigated away
+ useEffect(() => {
+ clearOnNavRef.current = { readTimeout, nest, whom, markRead };
+ }, [readTimeout, nest, whom, markRead]);
+
+ useEffect(
+ () => () => {
+ const curr = clearOnNavRef.current;
+ if (curr.readTimeout !== undefined && curr.readTimeout !== 0) {
+ useChatStore.getState().read(curr.whom);
+ curr.markRead({ nest: curr.nest });
+ }
+ },
+ []
);
- }
- if (!compatible && messages.length === 0) {
+ useEffect(() => {
+ const doRefetch = async () => {
+ remove();
+ await refetch();
+ };
+
+ // If we have a scrollTo, we have a next page, and the scrollTo message is
+ // not in our current set of messages, that means we're scrolling to a
+ // message that's not yet cached. So, we need to refetch (which would fetch
+ // messages around the scrollTo time), then scroll to the message.
+ if (scrollToId && hasNextPage && !msgIdTimeInMessages) {
+ doRefetch();
+ }
+ }, [scrollToId, hasNextPage, remove, refetch, msgIdTimeInMessages]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!compatible && messages.length === 0) {
+ return (
+
+
+
+ There may be content in this channel, but it is inaccessible
+ because the host is using an older, incompatible version of the
+ app.
+
+ Please try again later.
+
+
+ );
+ }
+
return (
-
-
-
- There may be content in this channel, but it is inaccessible because
- the host is using an older, incompatible version of the app.
-
- Please try again later.
-
+
+
+
+
+
+ {scrollToId &&
+ (hasPreviousPage || latestIsMoreThan30NewerThanScrollTo) ? (
+
+ ) : null}
);
}
+);
- return (
-
-
-
-
-
- {scrollToId &&
- (hasPreviousPage || latestIsMoreThan30NewerThanScrollTo) ? (
-
- ) : null}
-
- );
-}
+export default ChatWindow;
diff --git a/apps/tlon-web/src/logic/useIsEditingMessage.tsx b/apps/tlon-web/src/logic/useIsEditingMessage.tsx
index e3c414e497..896bae3298 100644
--- a/apps/tlon-web/src/logic/useIsEditingMessage.tsx
+++ b/apps/tlon-web/src/logic/useIsEditingMessage.tsx
@@ -3,7 +3,10 @@ import { useSearchParams } from 'react-router-dom';
export default function useIsEditingMessage() {
const [searchParams] = useSearchParams();
- const isEditing = useMemo(() => !!searchParams.get('edit'), [searchParams]);
+ const isEditing = useMemo(
+ () => !!searchParams.get('edit') || !!searchParams.get('editReply'),
+ [searchParams]
+ );
return isEditing;
}
diff --git a/apps/tlon-web/src/replies/ReplyMessage.tsx b/apps/tlon-web/src/replies/ReplyMessage.tsx
index 1a223ba434..7153e7190c 100644
--- a/apps/tlon-web/src/replies/ReplyMessage.tsx
+++ b/apps/tlon-web/src/replies/ReplyMessage.tsx
@@ -126,7 +126,7 @@ const ReplyMessage = React.memo<
ref
) => {
const [searchParms, setSearchParams] = useSearchParams();
- const isEditing = searchParms.get('edit') === reply.seal.id;
+ const isEditing = searchParms.get('editReply') === reply.seal.id;
const isEdited = useIsEdited(reply);
const { seal, memo } = reply.seal.id ? reply : emptyReply;
const container = useRef
(null);
diff --git a/apps/tlon-web/src/replies/ReplyMessageOptions.tsx b/apps/tlon-web/src/replies/ReplyMessageOptions.tsx
index faa2bb51bc..62b141b01e 100644
--- a/apps/tlon-web/src/replies/ReplyMessageOptions.tsx
+++ b/apps/tlon-web/src/replies/ReplyMessageOptions.tsx
@@ -173,7 +173,7 @@ export default function ReplyMessageOptions(props: {
}, [doCopy, isMobile, onOpenChange]);
const edit = useCallback(() => {
- setSearchParams({ edit: seal.id }, { replace: true });
+ setSearchParams({ editReply: seal.id }, { replace: true });
}, [seal, setSearchParams]);
const setReplyParam = useCallback(() => {
diff --git a/desk/desk.docket-0 b/desk/desk.docket-0
index 9bcf9cdc2b..dce594c6b0 100644
--- a/desk/desk.docket-0
+++ b/desk/desk.docket-0
@@ -2,7 +2,7 @@
info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.'
color+0xde.dede
image+'https://bootstrap.urbit.org/tlon.svg?v=1'
- glob-http+['https://bootstrap.urbit.org/glob-0vcdj6c.jc8nr.avuag.uhh4v.vqc33.glob' 0vcdj6c.jc8nr.avuag.uhh4v.vqc33]
+ glob-http+['https://bootstrap.urbit.org/glob-0v5.dvi7n.9bcsc.hid09.mhlbr.jr4l5.glob' 0v5.dvi7n.9bcsc.hid09.mhlbr.jr4l5]
base+'groups'
version+[5 9 0]
website+'https://tlon.io'