Skip to content

Commit

Permalink
feat: select contacts dialog, chats unread events, misc
Browse files Browse the repository at this point in the history
  • Loading branch information
Dwynr committed Apr 22, 2024
1 parent 1a8c962 commit 34663ee
Show file tree
Hide file tree
Showing 29 changed files with 524 additions and 89 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 45 additions & 5 deletions src/components/chats/conversation/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,17 @@ export const searchEmojiIndex = memoize((query: string): Promise<{ skins?: { src

export const Input = memo(({ conversation }: { conversation: ChatConversation }) => {
const [editor] = useState<BaseEditor & ReactEditor & HistoryEditor>(() => 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<ReturnType<typeof setTimeout>>()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -290,7 +328,9 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation })
getEditorText,
replyMessage,
errorToast,
hideSuggestions
hideSuggestions,
setSelectedConversation,
setConversations
])

const editMessage = useCallback(async (): Promise<void> => {
Expand Down Expand Up @@ -813,7 +853,7 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation })
return (
<div
className={cn(
"flex flex-row items-center justify-between py-2 px-2 rounded-lg hover:bg-primary-foreground cursor-pointer",
"flex flex-row items-center justify-between py-2 px-2 rounded-md hover:bg-primary-foreground cursor-pointer",
index === mentionsSuggestionsIndex ? "bg-primary-foreground" : "bg-transparent"
)}
key={participant.userId}
Expand Down Expand Up @@ -863,7 +903,7 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation })
return (
<div
className={cn(
"flex flex-row items-center justify-between py-2 px-2 rounded-lg hover:bg-primary-foreground cursor-pointer",
"flex flex-row items-center justify-between py-2 px-2 rounded-md hover:bg-primary-foreground cursor-pointer",
index === emojisSuggestionsIndex ? "bg-primary-foreground" : "bg-transparent"
)}
key={shortCode + ":" + index}
Expand Down Expand Up @@ -905,7 +945,7 @@ export const Input = memo(({ conversation }: { conversation: ChatConversation })
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
placeholder={t("chats.input.placeholder")}
className="slate-editor z-10 border rounded-lg shadow-sm bg-background w-full min-h-10 max-h-[40vh] overflow-y-auto overflow-x-hidden pl-11 pr-11 py-3 break-all outline-none focus:outline-none active:outline-none hover:outline-none"
className="slate-editor z-10 border rounded-md shadow-sm bg-background w-full min-h-10 max-h-[40vh] overflow-y-auto overflow-x-hidden pl-11 pr-11 py-3 break-all outline-none focus:outline-none active:outline-none hover:outline-none"
autoCorrect="none"
autoCapitalize="none"
autoFocus={false}
Expand Down
23 changes: 18 additions & 5 deletions src/components/chats/conversation/markAsRead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useSDKConfig from "@/hooks/useSDKConfig"
import { useTranslation } from "react-i18next"
import { simpleDate } from "@/utils"
import eventEmitter from "@/lib/eventEmitter"
import { useChatsStore } from "@/stores/chats.store"

export const MarkAsRead = memo(
({
Expand All @@ -27,6 +28,7 @@ export const MarkAsRead = memo(
const [markingAsRead, setMarkingAsRead] = useState<boolean>(false)
const sdkConfig = useSDKConfig()
const { t } = useTranslation()
const { setConversationsUnread } = useChatsStore()

const { show, count, since } = useMemo(() => {
if (messagesSorted.length === 0) {
Expand Down Expand Up @@ -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)
Expand All @@ -105,7 +118,7 @@ export const MarkAsRead = memo(
onClick={markAsRead}
>
<p className="line-clamp-1 text-ellipsis break-all flex flex-row items-center">
{t(count <= 1 ? "chats.newMessageSince" : "chats.newMessagesSince", { count, since })}
{t(count <= 1 ? "chats.newMessageSince" : "chats.newMessagesSince", { count: count >= 99 ? 99 : count, since })}
</p>
<p className="line-clamp-1 text-ellipsis break-all flex flex-row items-center gap-2 shrink-0">
{markingAsRead ? (
Expand Down
2 changes: 1 addition & 1 deletion src/components/chats/conversation/message/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const ReplaceMessageWithComponents = memo(
return (
<div
key={index}
className="flex-col max-w-full p-2 py-1 bg-secondary border rounded-lg shadow-sm basis-full"
className="flex-col max-w-full p-2 py-1 bg-secondary border rounded-md shadow-sm basis-full"
>
{code}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/chats/conversation/topBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const TopBar = memo(({ conversation }: { conversation: ChatConversation }
<Tooltip>
<TooltipTrigger asChild={true}>
<div
className="hover:bg-secondary rounded-lg p-1 cursor-pointer"
className="hover:bg-secondary rounded-md p-1 cursor-pointer"
onClick={toggleParticipantsContainer}
>
{conversationParticipantsContainerOpen ? <ChevronRight /> : <ChevronLeft />}
Expand Down
60 changes: 33 additions & 27 deletions src/components/chats/participants/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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,
Expand All @@ -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 (
<div className="w-full h-full flex flex-col">
<div className="w-full h-12 flex flex-row items-center justify-between px-4">
Expand All @@ -32,8 +55,8 @@ export const Participants = memo(({ conversation }: { conversation: ChatConversa
<Tooltip>
<TooltipTrigger asChild={true}>
<div
className="hover:bg-secondary rounded-lg p-1 cursor-pointer"
onClick={() => {}}
className="hover:bg-secondary rounded-md p-1 cursor-pointer"
onClick={addParticipant}
>
<Plus />
</div>
Expand Down Expand Up @@ -69,28 +92,11 @@ export const Participants = memo(({ conversation }: { conversation: ChatConversa
data-index={virtualItem.index}
ref={rowVirtualizer.measureElement}
>
<ContextMenu
participant={participant}
<Participant
conversation={conversation}
>
<div className="flex flex-row items-center p-3 gap-3 cursor-pointer hover:bg-primary-foreground">
<Avatar
className="w-7 h-7"
src={participant.avatar}
fallback={participant.email}
status="online"
/>
<div className="flex flex-row items-center gap-3">
<p className="line-clamp-1 text-ellipsis break-all">{participant.email}</p>
{participant.userId === conversation.ownerId && (
<Crown
size={16}
className="text-yellow-500 shrink-0"
/>
)}
</div>
</div>
</ContextMenu>
onlineUsers={onlineQuery.data}
participant={participant}
/>
</div>
)
})}
Expand Down
57 changes: 57 additions & 0 deletions src/components/chats/participants/participant.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ContextMenu
participant={participant}
conversation={conversation}
>
<div className="flex flex-row items-center p-3 gap-3 cursor-pointer hover:bg-primary-foreground">
<Avatar
className="w-7 h-7"
src={participant.avatar}
fallback={participant.email}
status={status}
/>
<div className="flex flex-row items-center gap-3">
<p className="line-clamp-1 text-ellipsis break-all">{participant.email}</p>
{participant.userId === conversation.ownerId && (
<Crown
size={16}
className="text-yellow-500 shrink-0"
/>
)}
</div>
</div>
</ContextMenu>
)
}
)

export default Participant
42 changes: 42 additions & 0 deletions src/components/dialogs/selectContacts/contact.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<ContactType[]>>
contact: ContactType
}) => {
const isSelected = useMemo(() => {
return responseContacts.some(c => c.uuid === contact.uuid)
}, [responseContacts, contact.uuid])

return (
<div
className={cn(
"flex flex-row gap-2 items-center hover:bg-secondary p-2 rounded-md cursor-pointer justify-between",
isSelected && "bg-secondary"
)}
onClick={() =>
isSelected
? setResponseContacts(prev => prev.filter(c => c.uuid !== contact.uuid))
: setResponseContacts(prev => [...prev.filter(c => c.uuid !== contact.uuid), contact])
}
>
<div className="flex flex-row gap-2 items-center">
<Avatar src={contact.avatar} />
<p className="line-clamp-1 text-ellipsis break-all">{contact.nickName.length > 0 ? contact.nickName : contact.email}</p>
</div>
<p className="line-clamp-1 text-ellipsis break-all text-sm text-muted-foreground">{contact.email}</p>
</div>
)
}
)

export default Contact
Loading

0 comments on commit 34663ee

Please sign in to comment.