Skip to content

Commit

Permalink
Merge pull request #1128 from The-Commit-Company/ability-to-pin-messa…
Browse files Browse the repository at this point in the history
…ges-in-a-channel

feat: pinned messages
  • Loading branch information
nikkothari22 authored Jan 6, 2025
2 parents 1b0d513 + bb5d030 commit 4b67115
Show file tree
Hide file tree
Showing 23 changed files with 335 additions and 24 deletions.
10 changes: 5 additions & 5 deletions frontend/src/components/feature/GlobalSearch/MessageBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DateMonthYear } from "@/utils/dateConversions"

type MessageBoxProps = {
message: Message & { workspace?: string }
handleScrollToMessage: (messageName: string, channelID: string, workspace?: string) => void
handleScrollToMessage?: (messageName: string, channelID: string, workspace?: string) => void
}

export const MessageBox = ({ message, handleScrollToMessage }: MessageBoxProps) => {
Expand Down Expand Up @@ -44,15 +44,15 @@ export const MessageBox = ({ message, handleScrollToMessage }: MessageBoxProps)
<Separator orientation='vertical' />
<Text as='span' size='1' color='gray'><DateMonthYear date={creation} /></Text>

<Link size='1' className="invisible group-hover:visible cursor-pointer" onClick={() => handleScrollToMessage(message.name, channel_id, message.workspace)}>
{handleScrollToMessage ? <Link size='1' className="invisible group-hover:visible cursor-pointer" onClick={() => handleScrollToMessage(message.name, channel_id, message.workspace)}>
View in channel
</Link>
</Link> : null}
</Flex>

<Flex gap='3'>
<MessageSenderAvatar userID={owner} user={user} isActive={false} />
<Flex direction='column' gap='1' justify='center'>
<Box mt='-1'>
<Flex direction='column' gap='0' justify='center'>
<Box>
<UserHoverCard user={user} userID={owner} isActive={false} />
</Box>
<MessageContent message={message} user={user} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import { useState } from "react"
import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog'
import { useIsDesktop } from '@/hooks/useMediaQuery'
import { DrawerContent, DrawerTrigger, Drawer } from '@/components/layout/Drawer'
import clsx from 'clsx'

interface EditChannelNameButtonProps extends IconButtonProps {
channelID: string,
channel_name: string,
channelType: ChannelListItem['type']
channelType: ChannelListItem['type'],
buttonVisible?: boolean,
}

export const EditChannelNameButton = ({ channelID, channel_name, channelType, ...props }: EditChannelNameButtonProps) => {
export const EditChannelNameButton = ({ channelID, channel_name, channelType, buttonVisible = false, ...props }: EditChannelNameButtonProps) => {

const [open, setOpen] = useState(false);

Expand All @@ -31,7 +33,7 @@ export const EditChannelNameButton = ({ channelID, channel_name, channelType, ..
<IconButton
variant="ghost"
color="gray"
className='invisible group-hover:visible'
className={clsx(buttonVisible ? '' : 'invisible group-hover:visible')}
aria-label="Click to edit channel name"
title='Edit channel name'
{...props}>
Expand All @@ -53,7 +55,7 @@ export const EditChannelNameButton = ({ channelID, channel_name, channelType, ..
<IconButton
variant="ghost"
color="gray"
className='invisible group-hover:visible'
className={clsx(buttonVisible ? '' : 'invisible group-hover:visible')}
aria-label="Click to edit channel name"
title='Edit channel name'
{...props}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ChannelHeaderMenu from "./ChannelHeaderMenu"
import { ViewChannelMemberAvatars } from "./ViewChannelMemberAvatars"
import { BiChevronLeft } from "react-icons/bi"
import { Link } from "react-router-dom"
import { ViewPinnedMessagesButton } from "../pinned-messages/ViewPinnedMessagesButton"

interface ChannelHeaderProps {
channelData: ChannelListItem
Expand Down Expand Up @@ -34,7 +35,8 @@ export const ChannelHeader = ({ channelData }: ChannelHeaderProps) => {
}}
className="mb-0.5 text-ellipsis line-clamp-1">{channelData.channel_name}</Heading>
</Flex>
<EditChannelNameButton channelID={channelData.name} channel_name={channelData.channel_name} channelType={channelData.type} disabled={channelData.is_archived == 1} />
<EditChannelNameButton channelID={channelData.name} channel_name={channelData.channel_name} channelType={channelData.type} disabled={channelData.is_archived == 1} buttonVisible={!!channelData.pinned_messages_string} />
<ViewPinnedMessagesButton pinnedMessagesString={channelData.pinned_messages_string ?? ''} />
</Flex>
</Flex>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MessageContent, MessageSenderAvatar, UserHoverCard } from "../MessageIt
import { Box, BoxProps, ContextMenu, Flex, Text } from "@radix-ui/themes"
import { MessageReactions } from "../MessageReactions"
import { DateTooltip, DateTooltipShort } from "../Renderers/DateTooltip"
import { RiShareForwardFill } from "react-icons/ri"
import { RiPushpinFill, RiShareForwardFill } from "react-icons/ri"
import { ReplyMessageBox } from "../ReplyMessageBox/ReplyMessageBox"
import { useContext, useMemo, useState } from "react"
import clsx from "clsx"
Expand Down Expand Up @@ -106,7 +106,7 @@ export const LeftRightLayout = ({ message, user, isActive, isHighlighted, onRepl
</Flex> : null}

{message.is_forwarded === 1 && <Flex className='text-gray-10 text-xs' gap={'1'} align={'center'}><RiShareForwardFill size='12' /> forwarded</Flex>}

{message.is_pinned === 1 && <Flex className='text-accent-9 text-xs' gap={'1'} align={'center'}><RiPushpinFill size='12' /> Pinned</Flex>}
{linked_message && replied_message_details && <ReplyMessageBox
className='sm:max-w-[32rem] max-w-[80vw] cursor-pointer mb-1'
role='button'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AiOutlineEdit } from 'react-icons/ai'
import { LuForward, LuReply } from 'react-icons/lu'
import { MdOutlineEmojiEmotions } from "react-icons/md";
import { CreateThreadContextItem } from './QuickActions/CreateThreadButton'
import { RiPushpinLine, RiUnpinLine } from 'react-icons/ri'
import MessageActionSubMenu from './MessageActionSubMenu'

export interface MessageContextMenuProps {
Expand Down Expand Up @@ -90,6 +91,7 @@ export const MessageContextMenu = ({ message, onDelete, onEdit, onReply, onForwa
</ContextMenu.Group>
}

<PinMessageAction message={message} />
<SaveMessageAction message={message} />

</ContextMenu.Group>
Expand Down Expand Up @@ -165,4 +167,32 @@ const SaveMessageAction = ({ message }: { message: Message }) => {
</ContextMenu.Item>


}


const PinMessageAction = ({ message }: { message: Message }) => {

const isPinned = message.is_pinned
const { call } = useContext(FrappeContext) as FrappeConfig

const handlePin = () => {
call.post('raven.api.raven_channel.toggle_pin_message', {
channel_id: message.channel_id,
message_id: message.name,
}).then(() => {
toast.success(`Message ${isPinned ? 'unpinned' : 'pinned'}`)
}).catch((e) => {
toast.error('Could not perform the action', {
description: getErrorMessage(e)
})
})
}

return <ContextMenu.Item onClick={handlePin}>
<Flex gap='2'>
{!isPinned ? <RiPushpinLine size='18' /> : <RiUnpinLine size='18' />}
{!isPinned ? "Pin" : "Unpin"}
</Flex>
</ContextMenu.Item>

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ReplyMessageBox } from './ReplyMessageBox/ReplyMessageBox'
import { generateAvatarColor } from '../../selectDropdowns/GenerateAvatarColor'
import { DoctypeLinkRenderer } from './Renderers/DoctypeLinkRenderer'
import { useDebounce } from '@/hooks/useDebounce'
import { RiRobot2Fill, RiShareForwardFill } from 'react-icons/ri'
import { RiPushpinFill, RiRobot2Fill, RiShareForwardFill } from 'react-icons/ri'
import { useIsDesktop } from '@/hooks/useMediaQuery'
import { useDoubleTap } from 'use-double-tap'
import useOutsideClick from '@/hooks/useOutsideClick'
Expand Down Expand Up @@ -179,6 +179,7 @@ export const MessageItem = ({ message, setDeleteMessage, isHighlighted, onReplyM
: null}
{/* Message content goes here */}
{message.is_forwarded === 1 && <Flex className='text-gray-10 text-xs' gap={'1'} align={'center'}><RiShareForwardFill size='12' /> forwarded</Flex>}
{message.is_pinned === 1 && <Flex className='text-accent-9 text-xs' gap={'1'} align={'center'}><RiPushpinFill size='12' /> Pinned</Flex>}
{/* If it's a reply, then show the linked message */}
{linked_message && replied_message_details && <ReplyMessageBox
className='sm:min-w-[32rem] cursor-pointer mb-1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const ChatBoxBody = ({ channelData }: ChatBoxBodyProps) => {
maxFileSize={10000000}>
<ChatStream
channelID={channelData.name}
pinnedMessagesString={channelData.pinned_messages_string}
replyToMessage={handleReplyAction}
/>
{canUserSendMessage &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,16 @@ import SystemMessageBlock from '../ChatMessage/SystemMessageBlock'
type Props = {
channelID: string,
replyToMessage: (message: Message) => void,
showThreadButton?: boolean
showThreadButton?: boolean,
pinnedMessagesString?: string
}

const ChatStream = ({ channelID, replyToMessage, showThreadButton = true }: Props) => {
const ChatStream = ({ channelID, replyToMessage, showThreadButton = true, pinnedMessagesString }: Props) => {


const scrollRef = useRef<HTMLDivElement | null>(null)

const { messages, hasOlderMessages, loadOlderMessages, goToLatestMessages, hasNewMessages, error, loadNewerMessages, isLoading, highlightedMessage, scrollToMessage } = useChatStream(channelID, scrollRef)
const { messages, hasOlderMessages, loadOlderMessages, goToLatestMessages, hasNewMessages, error, loadNewerMessages, isLoading, highlightedMessage, scrollToMessage } = useChatStream(channelID, scrollRef, pinnedMessagesString)
const { setDeleteMessage, ...deleteProps } = useDeleteMessage()

const { setEditMessage, ...editProps } = useEditMessage()
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/components/feature/chat/ChatStream/useChatStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type MessageDateBlock = Message | {
/**
* Hook to fetch messages to be rendered on the chat interface
*/
const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivElement | null>) => {
const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivElement | null>, pinnedMessagesString?: string) => {

const location = useLocation()
const navigate = useNavigate()
Expand Down Expand Up @@ -427,6 +427,9 @@ const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivEle
// Also format the date to be displayed in the chat interface
if (data) {

let pinnedMessageIDs = pinnedMessagesString?.split('\n') ?? []
pinnedMessageIDs = pinnedMessageIDs.map(messageID => messageID.trim())

const messages = [...data.message.messages]

// Messages are already sorted by date - from latest to oldest
Expand All @@ -445,7 +448,11 @@ const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivEle
name: currentDate
})

messagesWithDateSeparators.push({ ...messages[messages.length - 1], is_continuation: 0 })
messagesWithDateSeparators.push({
...messages[messages.length - 1],
is_continuation: 0,
is_pinned: pinnedMessageIDs.includes(messages[messages.length - 1].name) ? 1 : 0
})
// Loop through the messages and add date separators if the date changes
for (let i = messages.length - 2; i >= 0; i--) {

Expand All @@ -469,15 +476,17 @@ const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivEle
if (currentMessageSender !== nextMessageSender) {
messagesWithDateSeparators.push({
...message,
is_continuation: 0
is_continuation: 0,
is_pinned: pinnedMessageIDs.includes(message.name) ? 1 : 0
})
} else if (messageDateTime - currentDateTime > 120000) {
messagesWithDateSeparators.push({
...message,
is_continuation: 0
is_continuation: 0,
is_pinned: pinnedMessageIDs.includes(message.name) ? 1 : 0
})
} else {
messagesWithDateSeparators.push({ ...message, is_continuation: 1 })
messagesWithDateSeparators.push({ ...message, is_continuation: 1, is_pinned: pinnedMessageIDs.includes(message.name) ? 1 : 0 })
}
currentDate = messageDate
currentDateTime = new Date(message.creation).getTime()
Expand All @@ -489,7 +498,7 @@ const useChatStream = (channelID: string, scrollRef: MutableRefObject<HTMLDivEle
return []
}
}
}, [data])
}, [data, pinnedMessagesString])

const scrollToMessage = (messageID: string) => {
// Check if the message is in the messages array
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Dialog, Flex, IconButton, Text } from '@radix-ui/themes'
import { useFrappeGetCall } from 'frappe-react-sdk'
import { useParams } from 'react-router-dom'
import { Message } from '../../../../../types/Messaging/Message'
import { ErrorBanner } from '@/components/layout/AlertBanner/ErrorBanner'
import { MessageBox } from '../GlobalSearch/MessageBox'
import { IoClose } from 'react-icons/io5'

export const PinnedMessageModalContent = ({ onClose }: { onClose: () => void }) => {

const { channelID } = useParams<{ channelID: string }>()

const { data, error } = useFrappeGetCall<{ message: Message[] }>("raven.api.raven_message.get_pinned_messages", { 'channel_id': channelID }, undefined, {
revalidateOnFocus: false
})

return (
<>
<Dialog.Title>
<Flex justify={'between'} align={'center'}>
<Text>Pinned Messages</Text>
<IconButton variant='ghost' color='gray' aria-label='Close' onClick={onClose}>
<IoClose size='20' />
</IconButton>
</Flex>
</Dialog.Title>
<ErrorBanner error={error} />
<Flex direction='column' gap='3' justify='start'>
{data?.message?.map((message) => {
return (
<MessageBox key={message.name} message={message} />
)
})}
</Flex>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Button, Dialog } from '@radix-ui/themes'
import { RiPushpinLine } from 'react-icons/ri'
import { useState } from 'react'
import { DIALOG_CONTENT_CLASS } from '@/utils/layout/dialog'
import { PinnedMessageModalContent } from './PinnedMessageModalContent'

interface ViewPinnedMessagesButtonProps {
pinnedMessagesString: string
}

export const ViewPinnedMessagesButton = ({ pinnedMessagesString }: ViewPinnedMessagesButtonProps) => {

const pinnedMessages = pinnedMessagesString ? pinnedMessagesString.split('\n').length : 0

const [open, setOpen] = useState(false)

const onClose = () => {
setOpen(false)
}

if (!pinnedMessages) {
return null
}

return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>
<Button size='1' variant='soft' color='gray' aria-label="View pinned messages"
title='View pinned messages'>
<RiPushpinLine size='14' />{pinnedMessages}
</Button>
</Dialog.Trigger>
<Dialog.Content className={DIALOG_CONTENT_CLASS}>
<PinnedMessageModalContent onClose={onClose} />
</Dialog.Content>
</Dialog.Root>
)
}
15 changes: 15 additions & 0 deletions frontend/src/types/Raven/RavenPinnedMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

export interface RavenPinnedMessages{
name: string
creation: string
modified: string
owner: string
modified_by: string
docstatus: 0 | 1 | 2
parent?: string
parentfield?: string
parenttype?: string
idx?: number
/** Message ID : Link - Raven Message */
message_id: string
}
5 changes: 5 additions & 0 deletions frontend/src/types/RavenChannelManagement/RavenChannel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RavenPinnedMessages } from '../Raven/RavenPinnedMessages'

export interface RavenChannel{
creation: string
Expand Down Expand Up @@ -38,6 +39,10 @@ export interface RavenChannel{
last_message_timestamp?: string
/** Last Message Details : JSON */
last_message_details?: any
/** Pinned Messages : Table - Raven Pinned Messages */
pinned_messages?: RavenPinnedMessages[]
/** Pinned Messages String : Small Text */
pinned_messages_string?: string
/** Is AI Thread : Check */
is_ai_thread?: 0 | 1
/** OpenAI Thread ID : Data */
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/utils/channel/ChannelListProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { FrappeConfig, FrappeContext, FrappeError, useFrappeDocTypeEventListener, useFrappeGetCall } from 'frappe-react-sdk'
import { PropsWithChildren, createContext, useContext, useMemo } from 'react'
import { KeyedMutator } from 'swr'
import { RavenChannel } from '../../../../types/RavenChannelManagement/RavenChannel'
import { useSWRConfig } from 'frappe-react-sdk'
import { toast } from 'sonner'
import { getErrorMessage } from '@/components/layout/AlertBanner/ErrorBanner'
import { RavenChannel } from '@/types/RavenChannelManagement/RavenChannel'

export type UnreadChannelCountItem = { name: string, user_id?: string, unread_count: number, is_direct_message: 0 | 1 }

Expand All @@ -16,7 +16,7 @@ export type UnreadCountData = {

export type ChannelListItem = Pick<RavenChannel, 'name' | 'channel_name' | 'type' |
'channel_description' | 'is_direct_message' | 'is_self_message' |
'is_archived' | 'creation' | 'owner' | 'last_message_details' | 'last_message_timestamp' | 'workspace'>
'is_archived' | 'creation' | 'owner' | 'last_message_details' | 'last_message_timestamp' | 'workspace' | 'pinned_messages_string'>

export interface DMChannelListItem extends ChannelListItem {
peer_user_id: string,
Expand Down
Loading

0 comments on commit 4b67115

Please sign in to comment.