diff --git a/frontend/src/components/feature/channel-groups/UnreadList.tsx b/frontend/src/components/feature/channel-groups/UnreadList.tsx new file mode 100644 index 000000000..7ff368be2 --- /dev/null +++ b/frontend/src/components/feature/channel-groups/UnreadList.tsx @@ -0,0 +1,184 @@ +import { SidebarGroup, SidebarGroupItem, SidebarGroupLabel, SidebarGroupList } from "../../layout/Sidebar"; +import { ChannelItemElement } from '@/components/feature/channels/ChannelList'; +import { DirectMessageItemElement } from '../../feature/direct-messages/DirectMessageList'; +import { __ } from '@/utils/translations'; +import { useStickyState } from "@/hooks/useStickyState"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { SidebarBadge, SidebarViewMoreButton } from "@/components/layout/Sidebar/SidebarComp"; +import { Box, DropdownMenu, Flex, IconButton } from "@radix-ui/themes"; +import { ChannelWithUnreadCount, DMChannelWithUnreadCount } from "@/components/layout/Sidebar/useGetChannelUnreadCounts"; +import clsx from "clsx"; +import { UnreadCountData } from "@/utils/channel/ChannelListProvider"; +import { BiDotsVerticalRounded } from "react-icons/bi"; +import { useFrappePostCall, useSWRConfig } from "frappe-react-sdk"; +import { toast } from "sonner"; + +interface UnreadListProps { + unreadChannels: ChannelWithUnreadCount[] + unreadDMs: DMChannelWithUnreadCount[] +} + +export const UnreadList = ({ unreadChannels, unreadDMs }: UnreadListProps) => { + + const [showData, setShowData] = useStickyState(true, 'expandDirectMessageList') + + const toggle = () => setShowData(d => !d) + + const ref = useRef(null) + + const [height, setHeight] = useState(ref?.current?.clientHeight ?? showData ? (unreadDMs.length + unreadChannels.length) * (36) - 4 : 0) + + useLayoutEffect(() => { + setHeight(ref.current?.clientHeight ?? 0) + }, [unreadDMs, unreadChannels]) + + const { totalUnreadCount, channelIDs } = useMemo(() => { + let totalUnreadCount = 0 + let channelIDs = [] + + // Count unread messages from channels + for (const channel of unreadChannels) { + if (channel.is_archived == 0) { + totalUnreadCount += channel.unread_count || 0 + channelIDs.push(channel.name) + } + } + + // Count unread messages from DMs + for (const dm of unreadDMs) { + totalUnreadCount += dm.unread_count || 0 + channelIDs.push(dm.name) + } + + return { totalUnreadCount, channelIDs } + }, [unreadChannels, unreadDMs]) + + return ( + + + + + {__("Unread")} + 0 ? 'opacity-100' : 'opacity-0')}> + + {totalUnreadCount} + + + + + + + + + + +
+ {/* Render unread DMs */} + {unreadDMs.map(dm => ( + + ))} + {/* Render unread channels */} + {unreadChannels.map(channel => ( + + ))} +
+
+
+ ) +} + +const UnreadSectionActions = ({ channelIDs }: { channelIDs: string[] }) => { + + const { mutate } = useSWRConfig() + + const [isOpen, setIsOpen] = useState(false) + const { call } = useFrappePostCall('raven.api.raven_channel.mark_all_messages_as_read') + const handleMarkAllAsRead = () => { + call({ + channel_ids: channelIDs + }).then(() => { + toast.success('All messages marked as read') + mutate('unread_channel_count', (d: { message: UnreadCountData } | undefined) => { + if (d?.message) { + // Update all channels with unread count as 0 + const newChannels = d.message.channels.map(c => { + if (c.name && channelIDs.includes(c.name)) { + return { + ...c, + unread_count: 0 + } + } + return c + }) + + const total_unread_count_in_channels = newChannels.reduce((acc: number, c) => { + if (!c.is_direct_message) { + return acc + c.unread_count + } else { + return acc + } + }, 0) + + const total_unread_count_in_dms = newChannels.reduce((acc: number, c) => { + if (c.is_direct_message) { + return acc + c.unread_count + } else { + return acc + } + }, 0) + + return { + message: { + total_unread_count_in_channels, + total_unread_count_in_dms, + channels: newChannels + } + } + } + }, { + revalidate: false + }) + }).catch(() => { + toast.error('Failed to mark all messages as read') + }) + setIsOpen(false) + } + + return ( + setIsOpen(open)}> + + + + + + + Mark all as read + + + ) +} \ No newline at end of file diff --git a/frontend/src/components/feature/channels/ChannelList.tsx b/frontend/src/components/feature/channels/ChannelList.tsx index 5b7007997..15c2fa0a1 100644 --- a/frontend/src/components/feature/channels/ChannelList.tsx +++ b/frontend/src/components/feature/channels/ChannelList.tsx @@ -2,56 +2,37 @@ import { SidebarGroup, SidebarGroupItem, SidebarGroupLabel, SidebarGroupList, Si import { SidebarBadge, SidebarViewMoreButton } from "../../layout/Sidebar/SidebarComp" import { CreateChannelButton } from "./CreateChannelModal" import { useContext, useLayoutEffect, useMemo, useRef, useState } from "react" -import { ChannelListContext, ChannelListContextType, ChannelListItem, UnreadCountData } from "../../../utils/channel/ChannelListProvider" +import { ChannelListContext, ChannelListContextType } from "../../../utils/channel/ChannelListProvider" import { ChannelIcon } from "@/utils/layout/channelIcon" -import { Box, ContextMenu, Flex, Text } from "@radix-ui/themes" +import { ContextMenu, Flex, Text } from "@radix-ui/themes" import { useLocation, useParams } from "react-router-dom" import { useStickyState } from "@/hooks/useStickyState" import useCurrentRavenUser from "@/hooks/useCurrentRavenUser" import { RiPushpinLine, RiUnpinLine } from "react-icons/ri" import { FrappeConfig, FrappeContext } from "frappe-react-sdk" import { RavenUser } from "@/types/Raven/RavenUser" -import clsx from "clsx" import { __ } from "@/utils/translations" +import { ChannelWithUnreadCount } from "@/components/layout/Sidebar/useGetChannelUnreadCounts" -export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData }) => { +interface ChannelListProps { + channels: ChannelWithUnreadCount[] +} - const { channels, mutate } = useContext(ChannelListContext) as ChannelListContextType +export const ChannelList = ({ channels }: ChannelListProps) => { + const { mutate } = useContext(ChannelListContext) as ChannelListContextType const [showData, setShowData] = useStickyState(true, 'expandChannelList') const toggle = () => setShowData(d => !d) const { myProfile } = useCurrentRavenUser() - const { filteredChannels, totalUnreadCount } = useMemo(() => { - - const pinnedChannelIDs = myProfile?.pinned_channels?.map(pin => pin.channel_id) - - const channelList = [] - let totalUnreadCount = 0 - - for (const channel of channels) { - if (pinnedChannelIDs?.includes(channel.name)) { - continue - } - if (channel.is_archived == 0) { - const count = unread_count?.channels.find((unread) => unread.name === channel.name)?.unread_count - channelList.push({ - ...channel, - unread_count: count || 0 - }) - - totalUnreadCount += count || 0 - } - } + const pinnedChannelIDs = myProfile?.pinned_channels?.map(pin => pin.channel_id) - - return { - filteredChannels: channelList, - totalUnreadCount - } - }, [channels, myProfile, unread_count]) + // Filter channels based on pinned status + const filteredChannels = useMemo(() => { + return channels.filter(channel => !pinnedChannelIDs?.includes(channel.name)) + }, [channels, pinnedChannelIDs]) const ref = useRef(null) const [height, setHeight] = useState(ref?.current?.clientHeight ?? showData ? filteredChannels.length * (36) - 4 : 0) @@ -66,12 +47,6 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData } {__("Channels")} - 0 ? 'opacity-100' : 'opacity-0')}> - - {totalUnreadCount} - - @@ -86,7 +61,7 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData } }} >
- {filteredChannels.map((channel) => )}
@@ -96,17 +71,11 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData } ) } -interface ChannelListItemWithUnreadCount extends ChannelListItem { - unread_count: number -} - -const ChannelItem = ({ channel }: { channel: ChannelListItemWithUnreadCount, }) => { - +const ChannelItem = ({ channel }: { channel: ChannelWithUnreadCount }) => { return - } -export const ChannelItemElement = ({ channel }: { channel: ChannelListItemWithUnreadCount }) => { +export const ChannelItemElement = ({ channel }: { channel: ChannelWithUnreadCount }) => { const { channelID } = useParams() diff --git a/frontend/src/components/feature/direct-messages/DirectMessageList.tsx b/frontend/src/components/feature/direct-messages/DirectMessageList.tsx index 932d16911..cda1e2edb 100644 --- a/frontend/src/components/feature/direct-messages/DirectMessageList.tsx +++ b/frontend/src/components/feature/direct-messages/DirectMessageList.tsx @@ -6,20 +6,22 @@ import { SidebarBadge, SidebarItem, SidebarViewMoreButton } from "../../layout/S import { UserContext } from "../../../utils/auth/UserProvider" import { useGetUser } from "@/hooks/useGetUser" import { useIsUserActive } from "@/hooks/useIsUserActive" -import { ChannelListContext, ChannelListContextType, DMChannelListItem, UnreadCountData } from "../../../utils/channel/ChannelListProvider" -import { Box, Flex, Text } from "@radix-ui/themes" +import { ChannelListContext, ChannelListContextType } from "../../../utils/channel/ChannelListProvider" +import { Flex, Text } from "@radix-ui/themes" import { UserAvatar } from "@/components/common/UserAvatar" import { toast } from "sonner" import { getErrorMessage } from "@/components/layout/AlertBanner/ErrorBanner" import { useStickyState } from "@/hooks/useStickyState" -import clsx from "clsx" import { UserFields, UserListContext } from "@/utils/users/UserListProvider" import { replaceCurrentUserFromDMChannelName } from "@/utils/operations" import { __ } from "@/utils/translations" +import { DMChannelWithUnreadCount } from "@/components/layout/Sidebar/useGetChannelUnreadCounts" -export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCountData }) => { +interface DirectMessageListProps { + dm_channels: DMChannelWithUnreadCount[] +} - const { dm_channels } = useContext(ChannelListContext) as ChannelListContextType +export const DirectMessageList = ({ dm_channels }: DirectMessageListProps) => { const [showData, setShowData] = useStickyState(true, 'expandDirectMessageList') @@ -39,9 +41,6 @@ export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCount {__("Members")} - 0 ? 'opacity-100' : 'opacity-0')}> - {unread_count?.total_unread_count_in_dms} - @@ -52,7 +51,7 @@ export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCount height: showData ? height : 0 }}>
- + {dm_channels.length < 5 ? : null}
@@ -61,28 +60,22 @@ export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCount ) } -const DirectMessageItemList = ({ unread_count }: { unread_count?: UnreadCountData }) => { - const { dm_channels } = useContext(ChannelListContext) as ChannelListContextType - +const DirectMessageItemList = ({ dm_channels }: DirectMessageListProps) => { return <> - {dm_channels.map((channel) => )} + {dm_channels.map((channel: DMChannelWithUnreadCount) => ( + + ))} } -const DirectMessageItem = ({ channel, unreadCount }: { channel: DMChannelListItem, unreadCount: UnreadCountData['channels'] }) => { - - - const unreadCountForChannel = useMemo(() => unreadCount.find((unread) => unread.name == channel.name)?.unread_count, [channel.name, unreadCount]) - - return - +const DirectMessageItem = ({ dm_channel }: { dm_channel: DMChannelWithUnreadCount }) => { + return } -export const DirectMessageItemElement = ({ channel, unreadCount }: { channel: DMChannelListItem, unreadCount?: number }) => { +export const DirectMessageItemElement = ({ channel }: { channel: DMChannelWithUnreadCount }) => { const { currentUser } = useContext(UserContext) const userData = useGetUser(channel.peer_user_id) @@ -90,7 +83,7 @@ export const DirectMessageItemElement = ({ channel, unreadCount }: { channel: DM const { channelID } = useParams() - const showUnread = unreadCount && channelID !== channel.name + const showUnread = channel.unread_count && channelID !== channel.name if (!userData?.enabled) { // If the user does not exists or if the user exists, but is not enabled, don't show the item. @@ -117,7 +110,7 @@ export const DirectMessageItemElement = ({ channel, unreadCount }: { channel: DM }} className="text-ellipsis line-clamp-1" weight={showUnread ? 'bold' : 'medium'}> {channel.peer_user_id !== currentUser ? userData?.full_name ?? channel.peer_user_id ?? replaceCurrentUserFromDMChannelName(channel.channel_name, currentUser) : `${userData?.full_name} (You)`} - {showUnread ? {unreadCount} : null} + {showUnread ? {channel.unread_count} : null}
} diff --git a/frontend/src/components/layout/Sidebar/PinnedChannels.tsx b/frontend/src/components/layout/Sidebar/PinnedChannels.tsx index 86f1d897a..0eb83037b 100644 --- a/frontend/src/components/layout/Sidebar/PinnedChannels.tsx +++ b/frontend/src/components/layout/Sidebar/PinnedChannels.tsx @@ -1,7 +1,7 @@ import { ChannelListContext, ChannelListContextType, UnreadCountData } from '@/utils/channel/ChannelListProvider' import { useContext, useMemo } from 'react' import { SidebarGroup, SidebarGroupItem, SidebarGroupLabel, SidebarGroupList } from './SidebarComp' -import { Box, Flex } from '@radix-ui/themes' +import { Box } from '@radix-ui/themes' import { ChannelItemElement } from '@/components/feature/channels/ChannelList' import useCurrentRavenUser from '@/hooks/useCurrentRavenUser' import { __ } from '@/utils/translations' @@ -18,13 +18,13 @@ const PinnedChannels = ({ unread_count }: { unread_count?: UnreadCountData }) => return channels.filter(channel => pinnedChannelIDs?.includes(channel.name) && channel.is_archived === 0) .map(channel => { - const count = unread_count?.channels.find((unread) => unread.name === channel.name)?.unread_count + const count = unread_count?.channels.find((unread) => unread.name === channel.name)?.unread_count || 0 return { ...channel, - unread_count: count || 0 + unread_count: count } - }) + .filter(channel => channel.unread_count === 0) // Exclude channels with unread messages } else { return [] } diff --git a/frontend/src/components/layout/Sidebar/SidebarBody.tsx b/frontend/src/components/layout/Sidebar/SidebarBody.tsx index a226fb16b..e38fbbd06 100644 --- a/frontend/src/components/layout/Sidebar/SidebarBody.tsx +++ b/frontend/src/components/layout/Sidebar/SidebarBody.tsx @@ -4,13 +4,23 @@ import { SidebarItem } from './SidebarComp' import { AccessibleIcon, Box, Flex, ScrollArea, Text } from '@radix-ui/themes' import useUnreadMessageCount from '@/hooks/useUnreadMessageCount' import PinnedChannels from './PinnedChannels' -import React from 'react' +import React, { useContext } from 'react' import { BiBookmark, BiMessageAltDetail } from 'react-icons/bi' import { __ } from '@/utils/translations' +import { UnreadList } from '@/components/feature/channel-groups/UnreadList' +import { ChannelListContext, ChannelListContextType } from '@/utils/channel/ChannelListProvider' +import { useGetChannelUnreadCounts } from './useGetChannelUnreadCounts' export const SidebarBody = () => { const unread_count = useUnreadMessageCount() + const { channels, dm_channels } = useContext(ChannelListContext) as ChannelListContextType + + const { unreadChannels, readChannels, unreadDMs, readDMs } = useGetChannelUnreadCounts({ + channels, + dm_channels, + unread_count: unread_count?.message + }) return ( @@ -28,8 +38,9 @@ export const SidebarBody = () => { iconLabel='Saved Message' />
- - + {(unreadChannels.length > 0 || unreadDMs.length > 0) && } + + ) diff --git a/frontend/src/components/layout/Sidebar/useGetChannelUnreadCounts.ts b/frontend/src/components/layout/Sidebar/useGetChannelUnreadCounts.ts new file mode 100644 index 000000000..ee5c1fde2 --- /dev/null +++ b/frontend/src/components/layout/Sidebar/useGetChannelUnreadCounts.ts @@ -0,0 +1,61 @@ +import { ChannelListItem, DMChannelListItem, UnreadCountData } from '@/utils/channel/ChannelListProvider'; +import { useMemo } from 'react'; + +export interface UseGetChannelUnreadCountProps { + channels: ChannelListItem[] + dm_channels: DMChannelListItem[] + unread_count: UnreadCountData | undefined +} + +export interface ChannelWithUnreadCount extends ChannelListItem { + unread_count: number +} + +export interface DMChannelWithUnreadCount extends DMChannelListItem { + unread_count: number +} + +export const useGetChannelUnreadCounts = ({ channels, dm_channels, unread_count }: UseGetChannelUnreadCountProps) => { + + const { unreadChannels, readChannels, unreadDMs, readDMs } = useMemo(() => { + + const unreadCounts: Record = {} + + // Create a mapping of channel names to unread counts + unread_count?.channels?.forEach(item => { + unreadCounts[item.name] = item.unread_count || 0 + }) + + const unreadChannels: ChannelWithUnreadCount[] = [] + const readChannels: ChannelWithUnreadCount[] = [] + const unreadDMs: DMChannelWithUnreadCount[] = [] + const readDMs: DMChannelWithUnreadCount[] = [] + + const allChannels: (ChannelListItem | DMChannelListItem)[] = [...channels, ...dm_channels] + + // Process all channels and DMs to separate unread and read + allChannels.forEach(channel => { + const unreadCount = unreadCounts[channel.name] || 0 + const channelWithUnread = { ...channel, unread_count: unreadCount } + + if (unreadCount > 0) { + if ('is_direct_message' in channel && channel.is_direct_message) { + unreadDMs.push(channelWithUnread as DMChannelWithUnreadCount) + } else { + unreadChannels.push(channelWithUnread as ChannelWithUnreadCount) + } + } else { + if ('is_direct_message' in channel && channel.is_direct_message) { + readDMs.push(channelWithUnread as DMChannelWithUnreadCount) + } else { + readChannels.push(channelWithUnread as ChannelWithUnreadCount) + } + } + }); + + return { unreadChannels, readChannels, unreadDMs, readDMs } + + }, [channels, dm_channels, unread_count]) + + return { unreadChannels, readChannels, unreadDMs, readDMs } +} \ No newline at end of file diff --git a/raven/api/raven_channel.py b/raven/api/raven_channel.py index 5d035fe64..b94457a0d 100644 --- a/raven/api/raven_channel.py +++ b/raven/api/raven_channel.py @@ -3,6 +3,7 @@ from frappe.query_builder import Order from raven.api.raven_users import get_current_raven_user +from raven.utils import track_channel_visit @frappe.whitelist() @@ -195,3 +196,15 @@ def leave_channel(channel_id): frappe.delete_doc("Raven Channel Member", member.name) return "Ok" + + +@frappe.whitelist() +def mark_all_messages_as_read(channel_ids: list): + """ + Mark all messages in these channels as read + """ + user = frappe.session.user + for channel_id in channel_ids: + track_channel_visit(channel_id, user=user) + + return "Ok" \ No newline at end of file