Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added a section for 'unread' messages in the sidebar #1095

Merged
merged 6 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions frontend/src/components/feature/channel-groups/UnreadList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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 { useContext, 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 { ChannelListContext, ChannelListContextType } from "@/utils/channel/ChannelListProvider";
import { BiDotsVerticalRounded } from "react-icons/bi";
import { useFrappePostCall } from "frappe-react-sdk";
import { toast } from "sonner";

interface UnreadListProps {
unreadChannels: ChannelWithUnreadCount[]
unreadDMs: DMChannelWithUnreadCount[]
}

export const UnreadList = ({ unreadChannels, unreadDMs }: UnreadListProps) => {

const { mutate } = useContext(ChannelListContext) as ChannelListContextType
const [showData, setShowData] = useStickyState(true, 'expandDirectMessageList')

const toggle = () => setShowData(d => !d)

const ref = useRef<HTMLDivElement>(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 (
<SidebarGroup>
<SidebarGroupItem className={'gap-1 pl-1'}>
<Flex width='100%' justify='between' align='center' gap='2' pr='2' className="group">
<Flex align='center' gap='2' width='100%' onClick={toggle} className="cursor-default select-none">
<SidebarGroupLabel>{__("Unread")}</SidebarGroupLabel>
<Box className={clsx('transition-opacity ease-in-out duration-200',
!showData && totalUnreadCount > 0 ? 'opacity-100' : 'opacity-0')}>
<SidebarBadge>
{totalUnreadCount}
</SidebarBadge>
</Box>
</Flex>
<Flex align='center' gap='1'>
<UnreadSectionActions updateChannelList={mutate} channelIDs={channelIDs} />
<SidebarViewMoreButton onClick={toggle} expanded={showData} />
</Flex>
</Flex>
</SidebarGroupItem>
<SidebarGroupList
style={{
height: showData ? height : 0
}}>
<div ref={ref} className="flex gap-1 flex-col fade-in">
{/* Render unread DMs */}
{unreadDMs.map(dm => (
<DirectMessageItemElement
key={dm.name}
channel={dm}
/>
))}
{/* Render unread channels */}
{unreadChannels.map(channel => (
<ChannelItemElement
key={channel.name}
channel={channel}
/>
))}
</div>
</SidebarGroupList>
</SidebarGroup>
)
}

const UnreadSectionActions = ({ updateChannelList, channelIDs }: { updateChannelList: () => void, channelIDs: string[] }) => {

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')
updateChannelList()
}).catch(() => {
toast.error('Failed to mark all messages as read')
})
setIsOpen(false)
}

return (
<DropdownMenu.Root onOpenChange={(open) => setIsOpen(open)}>
<DropdownMenu.Trigger>
<IconButton
aria-label={__("Options")}
title={__("Options")}
variant="soft"
size="1"
radius="large"
className={clsx(
'cursor-pointer transition-all text-gray-10 dark:text-gray-300 bg-transparent',
'sm:hover:bg-gray-3',
{
'sm:invisible sm:group-hover:visible': !isOpen,
'sm:visible': isOpen, // Ensure it's visible when the dropdown is open
},
'ease-ease',
'outline-none'
)}>
<BiDotsVerticalRounded />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item onClick={handleMarkAllAsRead}>Mark all as read</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
63 changes: 16 additions & 47 deletions frontend/src/components/feature/channels/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null)
const [height, setHeight] = useState(ref?.current?.clientHeight ?? showData ? filteredChannels.length * (36) - 4 : 0)
Expand All @@ -66,12 +47,6 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData }
<Flex width='100%' justify='between' align='center' gap='2' pr='2' className="group">
<Flex align='center' gap='2' width='100%' onClick={toggle} className="cursor-default select-none">
<SidebarGroupLabel>{__("Channels")}</SidebarGroupLabel>
<Box className={clsx('transition-opacity ease-in-out duration-200',
!showData && unread_count && totalUnreadCount > 0 ? 'opacity-100' : 'opacity-0')}>
<SidebarBadge>
{totalUnreadCount}
</SidebarBadge>
</Box>
</Flex>
<Flex align='center' gap='1'>
<CreateChannelButton updateChannelList={mutate} />
Expand All @@ -86,7 +61,7 @@ export const ChannelList = ({ unread_count }: { unread_count?: UnreadCountData }
}}
>
<div ref={ref} className="flex gap-0.5 flex-col">
{filteredChannels.map((channel) => <ChannelItem
{filteredChannels.map((channel: ChannelWithUnreadCount) => <ChannelItem
channel={channel}
key={channel.name} />)}
</div>
Expand All @@ -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 <ChannelItemElement channel={channel} />

}

export const ChannelItemElement = ({ channel }: { channel: ChannelListItemWithUnreadCount }) => {
export const ChannelItemElement = ({ channel }: { channel: ChannelWithUnreadCount }) => {

const { channelID } = useParams()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -39,9 +41,6 @@ export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCount
<Flex width='100%' justify='between' align='center' gap='2' pr='2' className="group">
<Flex align='center' gap='2' width='100%' onClick={toggle} className="cursor-default select-none">
<SidebarGroupLabel className="pt-0.5">{__("Members")}</SidebarGroupLabel>
<Box className={clsx('transition-opacity ease-in-out duration-200', !showData && unread_count && unread_count?.total_unread_count_in_dms > 0 ? 'opacity-100' : 'opacity-0')}>
<SidebarBadge>{unread_count?.total_unread_count_in_dms}</SidebarBadge>
</Box>
</Flex>
<SidebarViewMoreButton onClick={toggle} expanded={showData} />
</Flex>
Expand All @@ -52,7 +51,7 @@ export const DirectMessageList = ({ unread_count }: { unread_count?: UnreadCount
height: showData ? height : 0
}}>
<div ref={ref} className="flex gap-1 flex-col fade-in">
<DirectMessageItemList unread_count={unread_count} />
<DirectMessageItemList dm_channels={dm_channels} />
{dm_channels.length < 5 ? <ExtraUsersItemList /> : null}
</div>
</SidebarGroupList>
Expand All @@ -61,36 +60,30 @@ 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) => <DirectMessageItem
key={channel.name}
channel={channel}
unreadCount={unread_count?.channels ?? []}
/>)}
{dm_channels.map((channel: DMChannelWithUnreadCount) => (
<DirectMessageItem
key={channel.name}
dm_channel={channel}
/>
))}
</>
}

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 <DirectMessageItemElement channel={channel} unreadCount={unreadCountForChannel} />

const DirectMessageItem = ({ dm_channel }: { dm_channel: DMChannelWithUnreadCount }) => {
return <DirectMessageItemElement channel={dm_channel} />
}

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)
const isActive = useIsUserActive(channel.peer_user_id)

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.
Expand All @@ -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)`}
</Text>
{showUnread ? <SidebarBadge>{unreadCount}</SidebarBadge> : null}
{showUnread ? <SidebarBadge>{channel.unread_count}</SidebarBadge> : null}
</Flex>
</SidebarItem>
}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/layout/Sidebar/PinnedChannels.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 []
}
Expand Down
Loading
Loading