diff --git a/package-lock.json b/package-lock.json index 8835f0e9..37772e2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@alptugidin/react-circular-progress-bar": "^1.1.2", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", - "@filen/sdk": "^0.1.55", + "@filen/sdk": "^0.1.58", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", @@ -1504,9 +1504,9 @@ } }, "node_modules/@filen/sdk": { - "version": "0.1.55", - "resolved": "https://registry.npmjs.org/@filen/sdk/-/sdk-0.1.55.tgz", - "integrity": "sha512-JRV8dskvPVlyvV3IdMYqsJ/P/smlxWymvrAjIxeuoXqisqX/ildb1CndZOCVfMGz8Q+opzOTqsaub+XNophO1g==", + "version": "0.1.58", + "resolved": "https://registry.npmjs.org/@filen/sdk/-/sdk-0.1.58.tgz", + "integrity": "sha512-VOsqVASvjO4Z4LnEyMLMcT1GljNsssR7H6z+mgpJwkx1J7gbhcgC8iU8VvCTQgyEMxeIq11LwqZ7JuDAISHFTw==", "dependencies": { "agentkeepalive": "^4.5.0", "axios": "^1.6.7", diff --git a/package.json b/package.json index 9d5b5eec..54708938 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@alptugidin/react-circular-progress-bar": "^1.1.2", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", - "@filen/sdk": "^0.1.55", + "@filen/sdk": "^0.1.58", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", diff --git a/src/components/chats/conversation/input.tsx b/src/components/chats/conversation/input.tsx index b96cc52f..58f7d276 100644 --- a/src/components/chats/conversation/input.tsx +++ b/src/components/chats/conversation/input.tsx @@ -41,7 +41,17 @@ export const searchEmojiIndex = memoize((query: string): Promise<{ skins?: { src export const Input = memo(({ conversation }: { conversation: ChatConversation }) => { const [editor] = useState(() => withReact(withHistory(createEditor()))) - const { setMessages, setFailedMessages, setEditUUID, setReplyMessage, replyMessage, editUUID, messages } = useChatsStore() + const { + setMessages, + setFailedMessages, + setEditUUID, + setReplyMessage, + replyMessage, + editUUID, + messages, + setSelectedConversation, + setConversations + } = useChatsStore() const { userId } = useSDKConfig() const { t } = useTranslation() const typingEventTimeout = useRef>() @@ -243,6 +253,34 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation }) } ]) + setConversations(prev => + prev.map(c => + c.uuid === conversation.uuid + ? { + ...c, + lastMessage: content, + lastMessageSender: me.userId, + lastMessageTimestamp: Date.now(), + lastMessageUUID: uuid + } + : c + ) + ) + + setSelectedConversation(prev => + prev + ? prev.uuid === conversation.uuid + ? { + ...prev, + lastMessage: content, + lastMessageSender: me.userId, + lastMessageTimestamp: Date.now(), + lastMessageUUID: uuid + } + : prev + : prev + ) + eventEmitter.emit("chatMarkAsRead") clearTimeout(typingEventTimeout.current) @@ -290,7 +328,9 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation }) getEditorText, replyMessage, errorToast, - hideSuggestions + hideSuggestions, + setSelectedConversation, + setConversations ]) const editMessage = useCallback(async (): Promise => { @@ -813,7 +853,7 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation }) return (
(false) const sdkConfig = useSDKConfig() const { t } = useTranslation() + const { setConversationsUnread } = useChatsStore() const { show, count, since } = useMemo(() => { if (messagesSorted.length === 0) { @@ -72,17 +74,28 @@ export const MarkAsRead = memo( setMarkingAsRead(true) try { - await worker.chatUpdateLastFocus({ - values: lastFocusQuery.data.map(lf => (lf.uuid === conversation.uuid ? { ...lf, lastFocus: Date.now() } : lf)) - }) + await Promise.all([ + worker.chatUpdateLastFocus({ + values: lastFocusQuery.data.map(lf => (lf.uuid === conversation.uuid ? { ...lf, lastFocus: Date.now() } : lf)) + }), + worker.chatMarkConversationAsRead({ conversation: conversation.uuid }) + ]) await lastFocusQuery.refetch() + + setConversationsUnread(prev => { + const newRecord = { ...prev } + + delete newRecord[conversation.uuid] + + return newRecord + }) } catch (e) { console.error(e) } finally { setMarkingAsRead(false) } - }, [lastFocusQuery, conversation.uuid, count]) + }, [lastFocusQuery, conversation.uuid, count, setConversationsUnread]) useEffect(() => { const chatMarkAsReadListener = eventEmitter.on("chatMarkAsRead", markAsRead) @@ -105,7 +118,7 @@ export const MarkAsRead = memo( onClick={markAsRead} >

- {t(count <= 1 ? "chats.newMessageSince" : "chats.newMessagesSince", { count, since })} + {t(count <= 1 ? "chats.newMessageSince" : "chats.newMessagesSince", { count: count >= 99 ? 99 : count, since })}

{markingAsRead ? ( diff --git a/src/components/chats/conversation/message/utils.tsx b/src/components/chats/conversation/message/utils.tsx index 9149bb71..3a2cc52d 100644 --- a/src/components/chats/conversation/message/utils.tsx +++ b/src/components/chats/conversation/message/utils.tsx @@ -141,7 +141,7 @@ export const ReplaceMessageWithComponents = memo( return (

{code}
diff --git a/src/components/chats/conversation/topBar.tsx b/src/components/chats/conversation/topBar.tsx index 1af7dd2d..da42ae1b 100644 --- a/src/components/chats/conversation/topBar.tsx +++ b/src/components/chats/conversation/topBar.tsx @@ -61,7 +61,7 @@ export const TopBar = memo(({ conversation }: { conversation: ChatConversation }
{conversationParticipantsContainerOpen ? : } diff --git a/src/components/chats/participants/index.tsx b/src/components/chats/participants/index.tsx index fb4f4d12..c66d4577 100644 --- a/src/components/chats/participants/index.tsx +++ b/src/components/chats/participants/index.tsx @@ -1,19 +1,28 @@ -import { memo, useRef } from "react" +import { memo, useRef, useCallback } from "react" import { type ChatConversation } from "@filen/sdk/dist/types/api/v3/chat/conversations" -import { Plus, Crown } from "lucide-react" +import { Plus } from "lucide-react" import { TOOLTIP_POPUP_DELAY } from "@/constants" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { useTranslation } from "react-i18next" import { useVirtualizer } from "@tanstack/react-virtual" import useWindowSize from "@/hooks/useWindowSize" -import Avatar from "@/components/avatar" -import ContextMenu from "./contextMenu" +import { useQuery } from "@tanstack/react-query" +import worker from "@/lib/worker" +import Participant from "./participant" +import { selectContacts } from "@/components/dialogs/selectContacts" export const Participants = memo(({ conversation }: { conversation: ChatConversation }) => { const { t } = useTranslation() const virtualizerParentRef = useRef(null) const windowSize = useWindowSize() + const onlineQuery = useQuery({ + queryKey: ["chatConversationOnline", conversation.uuid], + queryFn: () => worker.chatConversationOnline({ conversation: conversation.uuid }), + refetchInterval: 15000, + refetchIntervalInBackground: true + }) + const rowVirtualizer = useVirtualizer({ count: conversation.participants.length, getScrollElement: () => virtualizerParentRef.current, @@ -24,6 +33,20 @@ export const Participants = memo(({ conversation }: { conversation: ChatConversa overscan: 5 }) + const addParticipant = useCallback(async () => { + const selectedContacts = await selectContacts() + + if (selectedContacts.cancelled) { + return + } + + console.log(selectedContacts) + }, []) + + if (!onlineQuery.isSuccess) { + return null + } + return (
@@ -32,8 +55,8 @@ export const Participants = memo(({ conversation }: { conversation: ChatConversa
{}} + className="hover:bg-secondary rounded-md p-1 cursor-pointer" + onClick={addParticipant} >
@@ -69,28 +92,11 @@ export const Participants = memo(({ conversation }: { conversation: ChatConversa data-index={virtualItem.index} ref={rowVirtualizer.measureElement} > - -
- -
-

{participant.email}

- {participant.userId === conversation.ownerId && ( - - )} -
-
-
+ onlineUsers={onlineQuery.data} + participant={participant} + />
) })} diff --git a/src/components/chats/participants/participant.tsx b/src/components/chats/participants/participant.tsx new file mode 100644 index 00000000..76099412 --- /dev/null +++ b/src/components/chats/participants/participant.tsx @@ -0,0 +1,57 @@ +import { memo, useMemo } from "react" +import { Crown } from "lucide-react" +import Avatar from "@/components/avatar" +import ContextMenu from "./contextMenu" +import { type ChatConversationParticipant, type ChatConversation } from "@filen/sdk/dist/types/api/v3/chat/conversations" +import { type ChatConversationsOnlineUser } from "@filen/sdk/dist/types/api/v3/chat/conversations/online" + +export const ONLINE_TIMEOUT = 300000 + +export const Participant = memo( + ({ + participant, + onlineUsers, + conversation + }: { + participant: ChatConversationParticipant + onlineUsers: ChatConversationsOnlineUser[] + conversation: ChatConversation + }) => { + const status = useMemo(() => { + const filtered = onlineUsers.filter(p => p.userId === participant.userId) + + if (filtered.length === 0 || filtered[0].appearOffline) { + return "offline" + } + + return filtered[0].lastActive > 0 ? (filtered[0].lastActive > Date.now() - ONLINE_TIMEOUT ? "online" : "offline") : "offline" + }, [participant.userId, onlineUsers]) + + return ( + +
+ +
+

{participant.email}

+ {participant.userId === conversation.ownerId && ( + + )} +
+
+
+ ) + } +) + +export default Participant diff --git a/src/components/dialogs/selectContacts/contact.tsx b/src/components/dialogs/selectContacts/contact.tsx new file mode 100644 index 00000000..0e85ee6a --- /dev/null +++ b/src/components/dialogs/selectContacts/contact.tsx @@ -0,0 +1,42 @@ +import { memo, useMemo } from "react" +import Avatar from "@/components/avatar" +import { type Contact as ContactType } from "@filen/sdk/dist/types/api/v3/contacts" +import { cn } from "@/lib/utils" + +export const Contact = memo( + ({ + responseContacts, + setResponseContacts, + contact + }: { + responseContacts: ContactType[] + setResponseContacts: React.Dispatch> + contact: ContactType + }) => { + const isSelected = useMemo(() => { + return responseContacts.some(c => c.uuid === contact.uuid) + }, [responseContacts, contact.uuid]) + + return ( +
+ isSelected + ? setResponseContacts(prev => prev.filter(c => c.uuid !== contact.uuid)) + : setResponseContacts(prev => [...prev.filter(c => c.uuid !== contact.uuid), contact]) + } + > +
+ +

{contact.nickName.length > 0 ? contact.nickName : contact.email}

+
+

{contact.email}

+
+ ) + } +) + +export default Contact diff --git a/src/components/dialogs/selectContacts/index.tsx b/src/components/dialogs/selectContacts/index.tsx new file mode 100644 index 00000000..fa5e54b4 --- /dev/null +++ b/src/components/dialogs/selectContacts/index.tsx @@ -0,0 +1,126 @@ +import { memo, useState, useEffect, useRef, useCallback } from "react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog" +import { useTranslation } from "react-i18next" +import eventEmitter from "@/lib/eventEmitter" +import { type Contact } from "@filen/sdk/dist/types/api/v3/contacts" +import List from "./list" + +export type SelectContactsResponse = { cancelled: true } | { cancelled: false; contacts: Contact[] } + +export async function selectContacts(): Promise { + return await new Promise(resolve => { + const id = Math.random().toString(16).slice(2) + + const listener = eventEmitter.on("selectContactsResponse", ({ contacts, id: i }: { contacts: Contact[]; id: string }) => { + if (id !== i) { + return + } + + listener.remove() + + if (contacts.length === 0) { + resolve({ cancelled: true }) + + return + } + + resolve({ cancelled: false, contacts }) + }) + + eventEmitter.emit("openSelectContactsDialog", { id }) + }) +} + +export const SelectContactsDialog = memo(() => { + const [open, setOpen] = useState(false) + const { t } = useTranslation() + const requestId = useRef("") + const [responseContacts, setResponseContacts] = useState([]) + const didSubmit = useRef(false) + + const submit = useCallback(() => { + if (didSubmit.current) { + return + } + + didSubmit.current = true + + eventEmitter.emit("selectContactsResponse", { + contacts: responseContacts, + id: requestId.current + }) + + setOpen(false) + }, [responseContacts]) + + const cancel = useCallback(() => { + if (didSubmit.current) { + return + } + + didSubmit.current = true + + eventEmitter.emit("selectContactsResponse", { + contacts: [], + id: requestId.current + }) + + setOpen(false) + }, []) + + useEffect(() => { + const listener = eventEmitter.on("openSelectContactsDialog", ({ id }: { id: string }) => { + requestId.current = id + didSubmit.current = false + + setResponseContacts([]) + setOpen(true) + }) + + return () => { + listener.remove() + } + }, []) + + return ( + { + setOpen(openState) + cancel() + }} + > + + + {t("dialogs.selectContacts.title")} + + + + {t("dialogs.cancel")} + + {t("dialogs.selectContacts.submit")} + + + + + ) +}) + +export default SelectContactsDialog diff --git a/src/components/dialogs/selectContacts/list.tsx b/src/components/dialogs/selectContacts/list.tsx new file mode 100644 index 00000000..6adcdf0e --- /dev/null +++ b/src/components/dialogs/selectContacts/list.tsx @@ -0,0 +1,88 @@ +import { memo, useRef, useMemo } from "react" +import { useTranslation } from "react-i18next" +import worker from "@/lib/worker" +import { useQuery } from "@tanstack/react-query" +import { useVirtualizer } from "@tanstack/react-virtual" +import { type Contact as ContactType } from "@filen/sdk/dist/types/api/v3/contacts" +import Contact from "./contact" + +export const List = memo( + ({ + responseContacts, + setResponseContacts + }: { + responseContacts: ContactType[] + setResponseContacts: React.Dispatch> + }) => { + const { t } = useTranslation() + const virtualizerParentRef = useRef(null) + + const query = useQuery({ + queryKey: ["listContacts"], + queryFn: () => worker.listContacts() + }) + + const contactsSorted = useMemo(() => { + if (!query.isSuccess) { + return [] + } + + return query.data.sort((a, b) => b.lastActive - a.lastActive) + }, [query.isSuccess, query.data]) + + const rowVirtualizer = useVirtualizer({ + count: contactsSorted.length, + getScrollElement: () => virtualizerParentRef.current, + estimateSize: () => 48, + getItemKey(index) { + return contactsSorted[index].uuid + }, + overscan: 5 + }) + + return ( +
+ {query.isSuccess && contactsSorted.length === 0 ? ( +
+

{t("dialogs.selectContacts.listEmpty")}

+
+ ) : ( +
+ {rowVirtualizer.getVirtualItems().map(virtualItem => { + const contact = contactsSorted[virtualItem.index] + + return ( +
+ +
+ ) + })} +
+ )} +
+ ) + } +) + +export default List diff --git a/src/components/dialogs/selectDriveDestination/list/index.tsx b/src/components/dialogs/selectDriveDestination/list/index.tsx index f97c0542..e2f7308f 100644 --- a/src/components/dialogs/selectDriveDestination/list/index.tsx +++ b/src/components/dialogs/selectDriveDestination/list/index.tsx @@ -1,7 +1,6 @@ -import { memo, useState, useEffect, useRef, useMemo } from "react" +import { memo, useEffect, useRef, useMemo } from "react" import { useQuery } from "@tanstack/react-query" import { useVirtualizer } from "@tanstack/react-virtual" -import { type DriveCloudItem } from "@/components/drive" import worker from "@/lib/worker" import ListItem from "./listItem" import { useTranslation } from "react-i18next" @@ -9,7 +8,6 @@ import eventEmitter from "@/lib/eventEmitter" import { orderItemsByType } from "@/components/drive/utils" export const List = memo(({ pathname, setPathname }: { pathname: string; setPathname: React.Dispatch> }) => { - const [items, setItems] = useState([]) const lastPathname = useRef("") const virtualizerParentRef = useRef(null) const { t } = useTranslation() @@ -26,8 +24,12 @@ export const List = memo(({ pathname, setPathname }: { pathname: string; setPath }) const itemsOrdered = useMemo(() => { - return orderItemsByType({ items, type: "nameAsc" }) - }, [items]) + if (!query.isSuccess) { + return [] + } + + return orderItemsByType({ items: query.data, type: "nameAsc" }) + }, [query.isSuccess, query.data]) const rowVirtualizer = useVirtualizer({ count: itemsOrdered.length, @@ -39,12 +41,6 @@ export const List = memo(({ pathname, setPathname }: { pathname: string; setPath overscan: 5 }) - useEffect(() => { - if (query.isSuccess) { - setItems(query.data) - } - }, [query.isSuccess, query.data, setItems]) - useEffect(() => { // We have to manually refetch the query because the component does not remount, only the location pathname changes. if (lastPathname.current !== pathname && query.isSuccess) { @@ -77,7 +73,7 @@ export const List = memo(({ pathname, setPathname }: { pathname: string; setPath overflowY: "auto" }} > - {query.isSuccess && query.data.length === 0 ? ( + {query.isSuccess && itemsOrdered.length === 0 ? (

{t("dialogs.selectDriveDestination.listEmpty")}

@@ -89,19 +85,18 @@ export const List = memo(({ pathname, setPathname }: { pathname: string; setPath position: "relative" }} > - {query.isSuccess && - rowVirtualizer.getVirtualItems().map(virtualItem => { - const item = itemsOrdered[virtualItem.index] + {rowVirtualizer.getVirtualItems().map(virtualItem => { + const item = itemsOrdered[virtualItem.index] - return ( - - ) - })} + return ( + + ) + })}
)}
diff --git a/src/components/dialogs/selectDriveDestination/list/listItem.tsx b/src/components/dialogs/selectDriveDestination/list/listItem.tsx index e7e477e9..d23d9afe 100644 --- a/src/components/dialogs/selectDriveDestination/list/listItem.tsx +++ b/src/components/dialogs/selectDriveDestination/list/listItem.tsx @@ -57,7 +57,7 @@ export const ListItem = memo( height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)` }} - className="flex flex-row justify-between items-center hover:bg-secondary cursor-pointer px-3 rounded-lg gap-5 select-none" + className="flex flex-row justify-between items-center hover:bg-secondary cursor-pointer px-3 rounded-md gap-5 select-none" onClick={() => setPathname(prev => `${prev}/${item.uuid}`)} >
diff --git a/src/components/drive/list/index.tsx b/src/components/drive/list/index.tsx index 28aaf8d7..7e8ad9b9 100644 --- a/src/components/drive/list/index.tsx +++ b/src/components/drive/list/index.tsx @@ -36,7 +36,7 @@ export const List = memo(() => { const itemsOrdered = useMemo(() => { if (location.includes("recents")) { - return orderItemsByType({ items, type: "dateDesc" }) + return orderItemsByType({ items, type: "uploadDateDesc" }) } return orderItemsByType({ items, type: "nameAsc" }) diff --git a/src/components/drive/list/item/contextMenu/index.tsx b/src/components/drive/list/item/contextMenu/index.tsx index c6574809..9f01cb66 100644 --- a/src/components/drive/list/item/contextMenu/index.tsx +++ b/src/components/drive/list/item/contextMenu/index.tsx @@ -88,15 +88,15 @@ export const ContextMenu = memo(({ item, children }: { item: DriveCloudItem; chi return } - const toast = loadingToast() + const parent = await selectDriveDestination() - try { - const parent = await selectDriveDestination() + if (parent.cancelled) { + return + } - if (parent.cancelled) { - return - } + const toast = loadingToast() + try { const itemsToMove = selectedItems.filter(item => item.parent !== parent.uuid) const movedUUIDs = itemsToMove.map(item => item.uuid) diff --git a/src/components/drive/list/item/index.tsx b/src/components/drive/list/item/index.tsx index 0f970c42..07a834c8 100644 --- a/src/components/drive/list/item/index.tsx +++ b/src/components/drive/list/item/index.tsx @@ -352,7 +352,7 @@ export const ListItem = memo(
@@ -366,7 +366,7 @@ export const ListItem = memo( { onDragOver={onDragOver} onDragLeave={onDragLeave} onDragEnter={onDragEnter} - className="border border-dashed w-full h-full rounded-lg flex flex-col items-center justify-center" + className="border border-dashed w-full h-full rounded-md flex flex-col items-center justify-center" > {t("dropZone.cta")}
diff --git a/src/components/mainContainer/innerSideBar/button.tsx b/src/components/mainContainer/innerSideBar/button.tsx index 088caf17..bde95ff0 100644 --- a/src/components/mainContainer/innerSideBar/button.tsx +++ b/src/components/mainContainer/innerSideBar/button.tsx @@ -29,7 +29,7 @@ export const Button = memo(({ uuid }: { uuid: string }) => { }} draggable={false} className={cn( - "flex flex-row gap-3 w-full px-3 py-2 rounded-lg transition-all items-center hover:bg-accent text-primary cursor-pointer", + "flex flex-row gap-3 w-full px-3 py-2 rounded-md transition-all items-center hover:bg-accent text-primary cursor-pointer", routeParent === uuid || location === `/${uuid}` ? "bg-accent" : "bg-transparent" )} > diff --git a/src/components/mainContainer/innerSideBar/chats/chat/index.tsx b/src/components/mainContainer/innerSideBar/chats/chat/index.tsx index 970ffc9b..c7f4f3ec 100644 --- a/src/components/mainContainer/innerSideBar/chats/chat/index.tsx +++ b/src/components/mainContainer/innerSideBar/chats/chat/index.tsx @@ -5,6 +5,7 @@ import { type ChatConversation } from "@filen/sdk/dist/types/api/v3/chat/convers import ContextMenu from "./contextMenu" import Avatar from "@/components/avatar" import { ReplaceMessageWithComponentsInline } from "@/components/chats/conversation/message/utils" +import { useChatsStore } from "@/stores/chats.store" export const Chat = memo( ({ @@ -20,6 +21,12 @@ export const Chat = memo( userId: number routeParent: string }) => { + const { conversationsUnread } = useChatsStore() + + const unreadCount = useMemo(() => { + return conversationsUnread[conversation.uuid] ? conversationsUnread[conversation.uuid] : 0 + }, [conversationsUnread, conversation.uuid]) + const participantsWithoutUser = useMemo(() => { return conversation.participants.filter(p => p.userId !== userId) }, [conversation.participants, userId]) @@ -58,6 +65,11 @@ export const Chat = memo( src={participantsWithoutUser[0].avatar} fallback={participantsWithoutUser[0].email} /> + {unreadCount > 0 && ( +
+ {unreadCount} +
+ )}

diff --git a/src/components/mainContainer/innerSideBar/chats/index.tsx b/src/components/mainContainer/innerSideBar/chats/index.tsx index 76a0ffd6..7f9f1bce 100644 --- a/src/components/mainContainer/innerSideBar/chats/index.tsx +++ b/src/components/mainContainer/innerSideBar/chats/index.tsx @@ -18,7 +18,7 @@ import socket from "@/lib/socket" export const Chats = memo(() => { const virtualizerParentRef = useRef(null) const windowSize = useWindowSize() - const { conversations, setConversations, selectedConversation, setSelectedConversation } = useChatsStore() + const { conversations, setConversations, selectedConversation, setSelectedConversation, setConversationsUnread } = useChatsStore() const [, setLastSelectedChatsConversation] = useLocalStorage("lastSelectedChatsConversation", "") const navigate = useNavigate() const routeParent = useRouteParent() @@ -45,6 +45,19 @@ export const Chats = memo(() => { overscan: 5 }) + const fetchConversationUnreadCount = useCallback( + async (uuid: string) => { + try { + const count = await worker.chatConversationUnreadCount({ conversation: uuid }) + + setConversationsUnread(prev => ({ ...prev, [uuid]: count })) + } catch (e) { + console.error(e) + } + }, + [setConversationsUnread] + ) + const socketEventListener = useCallback( async (event: SocketEvent) => { try { @@ -90,6 +103,13 @@ export const Chats = memo(() => { : prev : prev ) + + if (routeParent !== event.data.conversation) { + setConversationsUnread(prev => ({ + ...prev, + [event.data.conversation]: prev[event.data.conversation] ? prev[event.data.conversation] + 1 : 1 + })) + } } else if ( event.type === "chatMessageDelete" || event.type === "chatMessageEdited" || @@ -110,7 +130,7 @@ export const Chats = memo(() => { console.error(e) } }, - [conversations, setConversations, query, setSelectedConversation, routeParent, navigate] + [conversations, setConversations, query, setSelectedConversation, routeParent, navigate, setConversationsUnread] ) useEffect(() => { @@ -143,8 +163,12 @@ export const Chats = memo(() => { queryUpdatedAtRef.current = query.dataUpdatedAt setConversations(query.data) + + for (const convo of query.data) { + fetchConversationUnreadCount(convo.uuid) + } } - }, [query.isSuccess, query.data, query.dataUpdatedAt, setConversations]) + }, [query.isSuccess, query.data, query.dataUpdatedAt, setConversations, fetchConversationUnreadCount]) useEffect(() => { socket.addListener("socketEvent", socketEventListener) diff --git a/src/components/mainContainer/innerSideBar/notes/note/index.tsx b/src/components/mainContainer/innerSideBar/notes/note/index.tsx index 9fde2abe..b0ef650b 100644 --- a/src/components/mainContainer/innerSideBar/notes/note/index.tsx +++ b/src/components/mainContainer/innerSideBar/notes/note/index.tsx @@ -76,7 +76,7 @@ export const Note = memo( return (

{tag.name}
diff --git a/src/components/mainContainer/innerSideBar/top/chats/index.tsx b/src/components/mainContainer/innerSideBar/top/chats/index.tsx index f186fe3e..60c5f098 100644 --- a/src/components/mainContainer/innerSideBar/top/chats/index.tsx +++ b/src/components/mainContainer/innerSideBar/top/chats/index.tsx @@ -28,7 +28,7 @@ export const Chats = memo(() => {
{}} > diff --git a/src/components/mainContainer/innerSideBar/top/notes/index.tsx b/src/components/mainContainer/innerSideBar/top/notes/index.tsx index 861cf3a9..151a8e81 100644 --- a/src/components/mainContainer/innerSideBar/top/notes/index.tsx +++ b/src/components/mainContainer/innerSideBar/top/notes/index.tsx @@ -52,7 +52,7 @@ export const Notes = memo(() => {
diff --git a/src/components/mainContainer/innerSideBar/top/notes/tags/index.tsx b/src/components/mainContainer/innerSideBar/top/notes/tags/index.tsx index e55ccaa5..7627d112 100644 --- a/src/components/mainContainer/innerSideBar/top/notes/tags/index.tsx +++ b/src/components/mainContainer/innerSideBar/top/notes/tags/index.tsx @@ -11,7 +11,7 @@ import { cn } from "@/lib/utils" import ContextMenu from "./contextMenu" export const tagClassName = - "flex flex-row gap-1 items-center justify-center px-2 py-1 rounded-lg bg-primary-foreground hover:bg-secondary cursor-pointer h-7 text-sm" + "flex flex-row gap-1 items-center justify-center px-2 py-1 rounded-md bg-primary-foreground hover:bg-secondary cursor-pointer h-7 text-sm" export const Tags = memo(() => { const { t } = useTranslation() diff --git a/src/components/mainContainer/innerSideBar/tree.tsx b/src/components/mainContainer/innerSideBar/tree.tsx index f5b06f5c..4efed4c2 100644 --- a/src/components/mainContainer/innerSideBar/tree.tsx +++ b/src/components/mainContainer/innerSideBar/tree.tsx @@ -37,7 +37,7 @@ export const Tree = memo(({ parent, depth, pathname }: { parent: string; depth: >
{
{ return await SDK.cloud().changeDirectoryColor({ uuid, color }) } + +export async function chatConversationOnline({ conversation }: { conversation: string }): Promise { + return await SDK.chats().conversationOnline({ conversation }) +} + +export async function listContacts(): Promise { + return await SDK.contacts().all() +} + +export async function chatConversationUnreadCount({ conversation }: { conversation: string }): Promise { + return await SDK.chats().conversationUnreadCount({ conversation }) +} + +export async function chatMarkConversationAsRead({ conversation }: { conversation: string }): Promise { + return await SDK.chats().markConversationAsRead({ conversation }) +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 97e008d2..a64ef473 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -19,6 +19,7 @@ import { register as registerServiceWorker } from "register-service-worker" import { setItem } from "@/lib/localForage" import InputDialog from "@/components/dialogs/input" import { connect as socketConnect } from "@/lib/socket" +import SelectContactsDialog from "@/components/dialogs/selectContacts" export const persistantQueryClient = new QueryClient({ defaultOptions: { @@ -131,6 +132,7 @@ export const Root = memo(() => { + ) : ( diff --git a/src/stores/chats.store.ts b/src/stores/chats.store.ts index 693102b7..b661dcb6 100644 --- a/src/stores/chats.store.ts +++ b/src/stores/chats.store.ts @@ -10,6 +10,8 @@ export type ChatsStore = { failedMessages: string[] editUUID: string replyMessage: ChatMessage | null + conversationsUnread: Record + unread: number setConversations: (fn: ChatConversation[] | ((prev: ChatConversation[]) => ChatConversation[])) => void setSelectedConversation: (fn: ChatConversation | null | ((prev: ChatConversation | null) => ChatConversation | null)) => void setSearch: (fn: string | ((prev: string) => string)) => void @@ -17,6 +19,8 @@ export type ChatsStore = { setFailedMessages: (fn: string[] | ((prev: string[]) => string[])) => void setEditUUID: (fn: string | ((prev: string) => string)) => void setReplyMessage: (fn: ChatMessage | null | ((prev: ChatMessage | null) => ChatMessage | null)) => void + setConversationsUnread: (fn: Record | ((prev: Record) => Record)) => void + setUnread: (fn: number | ((prev: number) => number)) => void } export const useChatsStore = create(set => ({ @@ -27,6 +31,8 @@ export const useChatsStore = create(set => ({ failedMessages: [], editUUID: "", replyMessage: null, + conversationsUnread: {}, + unread: 0, setConversations(fn) { set(state => ({ conversations: typeof fn === "function" ? fn(state.conversations) : fn })) }, @@ -47,5 +53,11 @@ export const useChatsStore = create(set => ({ }, setReplyMessage(fn) { set(state => ({ replyMessage: typeof fn === "function" ? fn(state.replyMessage) : fn })) + }, + setConversationsUnread(fn) { + set(state => ({ conversationsUnread: typeof fn === "function" ? fn(state.conversationsUnread) : fn })) + }, + setUnread(fn) { + set(state => ({ unread: typeof fn === "function" ? fn(state.unread) : fn })) } }))