From b1b21e30a46967b5768c0474068f102c6a653daa Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 15 Sep 2025 16:50:58 -0600 Subject: [PATCH] Refactor chat permissions and enhance edit capabilities - Introduced `useChatPermission` hook to manage chat permissions. - Updated components to utilize the new permission logic, allowing conditional rendering of edit options. - Adjusted `ChatInput` to display a view-only message when the user lacks edit permissions. - Cleaned up `package.json` formatting and added missing exports in context files. --- .../features/chat/ChatHeaderDropdown.tsx | 19 +++++-- apps/web/src/context/Chats/index.ts | 1 + .../src/context/Chats/useChatPermission.ts | 17 ++++++ .../ChatContent/ChatInput/ChatInput.tsx | 53 ++++++++++++------- .../ChatContent/ChatMessageOptions.tsx | 22 +++++--- .../ChatContent/ChatUserMessage.tsx | 6 ++- package.json | 6 +-- .../queries/reports/get-report-metadata.ts | 2 +- 8 files changed, 89 insertions(+), 37 deletions(-) create mode 100644 apps/web/src/context/Chats/useChatPermission.ts diff --git a/apps/web/src/components/features/chat/ChatHeaderDropdown.tsx b/apps/web/src/components/features/chat/ChatHeaderDropdown.tsx index af9946ee0..65b49fedd 100644 --- a/apps/web/src/components/features/chat/ChatHeaderDropdown.tsx +++ b/apps/web/src/components/features/chat/ChatHeaderDropdown.tsx @@ -4,7 +4,7 @@ import type { IBusterChat } from '@/api/asset_interfaces'; import { useGetChat } from '@/api/buster_rest/chats'; import { Dropdown, type IDropdownItems } from '@/components/ui/dropdown'; import { useGetChatId } from '@/context/Chats/useGetChatId'; -import { getIsEffectiveOwner } from '@/lib/share'; +import { canEdit, getIsEffectiveOwner } from '@/lib/share'; import { useDeleteChatSelectMenu, useDuplicateChatSelectMenu, @@ -32,6 +32,7 @@ export const ChatContainerHeaderDropdown: React.FC<{ const deleteChat = useDeleteChatSelectMenu({ chatId }); const isOwnerEffective = getIsEffectiveOwner(permission); + const canEditChat = canEdit(permission); const menuItem: IDropdownItems = useMemo(() => { return [ @@ -40,10 +41,20 @@ export const ChatContainerHeaderDropdown: React.FC<{ favoriteChat, openInNewTab, { type: 'divider' }, - duplicateChat, - deleteChat, + canEditChat && duplicateChat, + canEditChat && deleteChat, ].filter(Boolean) as IDropdownItems; - }, [chatId, duplicateChat, deleteChat, duplicateChat]); + }, [ + chatId, + isOwnerEffective, + canEditChat, + shareMenu, + renameChatTitle, + favoriteChat, + openInNewTab, + duplicateChat, + deleteChat, + ]); return ( diff --git a/apps/web/src/context/Chats/index.ts b/apps/web/src/context/Chats/index.ts index baf3fdcc0..68eedcd26 100644 --- a/apps/web/src/context/Chats/index.ts +++ b/apps/web/src/context/Chats/index.ts @@ -1,2 +1,3 @@ +export * from './useChatPermission'; export * from './useGetActiveChat'; export * from './useIsStreamingMessage'; diff --git a/apps/web/src/context/Chats/useChatPermission.ts b/apps/web/src/context/Chats/useChatPermission.ts new file mode 100644 index 000000000..bcf67cafe --- /dev/null +++ b/apps/web/src/context/Chats/useChatPermission.ts @@ -0,0 +1,17 @@ +import type { ShareRole } from '@buster/server-shared/share'; +import type { IBusterChat } from '../../api/asset_interfaces'; +import { useGetChat } from '../../api/buster_rest/chats'; + +const stablePermissionSelector = (chat: IBusterChat): ShareRole => chat.permission; + +export const useChatPermission = (chatId: string | undefined): ShareRole | undefined => { + const { data: permission } = useGetChat( + { id: chatId || '' }, + { + select: stablePermissionSelector, + enabled: !!chatId, + } + ); + + return permission; +}; diff --git a/apps/web/src/layouts/ChatLayout/ChatContent/ChatInput/ChatInput.tsx b/apps/web/src/layouts/ChatLayout/ChatContent/ChatInput/ChatInput.tsx index d36a61b6a..2248cb40c 100644 --- a/apps/web/src/layouts/ChatLayout/ChatContent/ChatInput/ChatInput.tsx +++ b/apps/web/src/layouts/ChatLayout/ChatContent/ChatInput/ChatInput.tsx @@ -1,8 +1,11 @@ import React, { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { InputTextAreaButton } from '@/components/ui/inputs/InputTextAreaButton'; -import { useIsStreamingMessage } from '@/context/Chats/useIsStreamingMessage'; +import { Text } from '@/components/ui/typography'; +import { useChatPermission, useIsStreamingMessage } from '@/context/Chats'; +import { useGetChatId } from '@/context/Chats/useGetChatId'; import { useIsChatMode } from '@/context/Chats/useMode'; import { cn } from '@/lib/classMerge'; +import { canEdit } from '@/lib/share'; import { inputHasText } from '@/lib/text'; import { useChatInputFlow } from '../../../../context/Chats/useChatInputFlow'; import { AIWarning } from './AIWarning'; @@ -11,12 +14,15 @@ export const ChatInput: React.FC = React.memo(() => { const textAreaRef = useRef(null); const isStreamingMessage = useIsStreamingMessage(); const hasChat = useIsChatMode(); + const chatId = useGetChatId(); + const permission = useChatPermission(chatId); + const canEditChat = canEdit(permission); const [inputValue, setInputValue] = useState(''); const disableSubmit = useMemo(() => { - return !inputHasText(inputValue) && !isStreamingMessage; - }, [inputValue, isStreamingMessage]); + return (!inputHasText(inputValue) && !isStreamingMessage) || !canEditChat; + }, [inputValue, isStreamingMessage, canEditChat]); const { onSubmitPreflight, onStopChat } = useChatInputFlow({ disableSubmit, @@ -44,22 +50,31 @@ export const ChatInput: React.FC = React.memo(() => { 'z-10 mx-3 mt-0.5 mb-2 flex min-h-fit flex-col items-center space-y-1.5 overflow-visible' )} > - - - + {!canEditChat ? ( +
+ + This chat is view-only. You don't have permission to send messages. + +
+ ) : ( + <> + + + + )} ); }); diff --git a/apps/web/src/layouts/ChatLayout/ChatContent/ChatMessageOptions.tsx b/apps/web/src/layouts/ChatLayout/ChatContent/ChatMessageOptions.tsx index 9c2cd6f53..c386b5775 100644 --- a/apps/web/src/layouts/ChatLayout/ChatContent/ChatMessageOptions.tsx +++ b/apps/web/src/layouts/ChatLayout/ChatContent/ChatMessageOptions.tsx @@ -12,8 +12,10 @@ import { ThumbsDown as ThumbsDownFilled } from '@/components/ui/icons/NucleoIcon import { AppTooltip } from '@/components/ui/tooltip'; import { Text } from '@/components/ui/typography'; import { useBusterNotifications } from '@/context/BusterNotifications'; +import { useChatPermission } from '@/context/Chats'; import { useMemoizedFn } from '@/hooks/useMemoizedFn'; import { formatDate } from '@/lib/date'; +import { canEdit } from '@/lib/share'; import { timeout } from '@/lib/timeout'; export const ChatMessageOptions: React.FC<{ @@ -24,6 +26,8 @@ export const ChatMessageOptions: React.FC<{ const { openConfirmModal } = useBusterNotifications(); const { mutateAsync: duplicateChat, isPending: isCopying } = useDuplicateChat(); const { mutateAsync: updateChatMessageFeedback } = useUpdateChatMessageFeedback(); + const permission = useChatPermission(chatId); + const canEditChat = canEdit(permission); const { data: feedback } = useGetChatMessage(messageId, { select: ({ feedback }) => feedback, }); @@ -65,14 +69,16 @@ export const ChatMessageOptions: React.FC<{ return (
- -
- {isStreamFinished && ( + {isStreamFinished && canEditChat && (