Skip to content

Commit e075722

Browse files
authored
Merge pull request #6 from ably/feat/add-granular-settings-control
Refactor: Add more granular settings to match chat capabilities.
2 parents de1adcb + 49ccb9c commit e075722

16 files changed

+387
-144
lines changed

.storybook/mocks/mock-ably-chat.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ const MockOverridesContext = createContext<MockOverrides>({});
4444

4545
const useMockOverrides = () => useContext(MockOverridesContext);
4646

47-
interface MockChatClientResponse {
48-
clientId: string;
49-
}
50-
5147
const mockPaginatedResultWithItems = (items: Message[]): PaginatedResult<Message> => {
5248
return {
5349
items,
@@ -383,8 +379,10 @@ export const useChatSettings = (): Partial<ChatSettingsContextType> => {
383379
const overrides = useMockOverrides();
384380

385381
const defaultSettings: ChatSettings = {
386-
allowMessageUpdates: true,
387-
allowMessageDeletes: true,
382+
allowMessageUpdatesOwn: true,
383+
allowMessageUpdatesAny: false,
384+
allowMessageDeletesOwn: true,
385+
allowMessageDeletesAny: false,
388386
allowMessageReactions: true,
389387
};
390388

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export default [
105105
'no-redeclare': 'off',
106106
'node/no-missing-import': 'off',
107107
'@typescript-eslint/prefer-nullish-coalescing': 'off',
108+
'unicorn/number-literal-case': 'off',
108109

109110
/* --- naming convention ------------------------------------------ */
110111
'@typescript-eslint/naming-convention': [

src/components/atoms/avatar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const getRandomColor = (text: string): string => {
4444
// Generate a deterministic hash from the text
4545
let hash = 0;
4646
for (let i = 0; i < text.length; i++) {
47-
hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xFFFFFFFF;
47+
hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xffffffff;
4848
}
4949

5050
const colorIndex = Math.abs(hash) % colors.length;

src/components/molecules/chat-message.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,19 @@ export interface ChatMessageProps {
5353

5454
/**
5555
* Optional callback triggered when the user saves an edited message.
56-
* Only called for messages owned by the current user.
5756
* @param message - The original message object being edited
5857
* @param newText - The updated message text after editing
5958
*/
6059
onEdit?: (message: Message, newText: string) => void;
6160

6261
/**
6362
* Optional callback triggered when the user confirms message deletion.
64-
* Only called for messages owned by the current user after confirmation dialog.
6563
* @param message - The message object to be deleted
6664
*/
6765
onDelete?: (message: Message) => void;
6866

6967
/**
7068
* Optional callback triggered when a user adds an emoji reaction to the message.
71-
* Can be called by any user, not just the message owner.
7269
* @param message - The message object receiving the reaction
7370
* @param emoji - The emoji character being added as a reaction
7471
*/

src/components/molecules/chat-window.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useMessages, usePresence } from '@ably/chat/react';
33
import { clsx } from 'clsx';
44
import React, { useCallback } from 'react';
55

6-
import { useChatSettings } from '../../hooks/use-chat-settings.tsx';
76
import { useMessageWindow } from '../../hooks/use-message-window.tsx';
87
import { ChatMessageList } from './chat-message-list.tsx';
98
import { ChatWindowFooter } from './chat-window-footer.tsx';
@@ -271,9 +270,6 @@ export const ChatWindow = ({
271270
className,
272271
onError,
273272
}: ChatWindowProps) => {
274-
const { getEffectiveSettings } = useChatSettings();
275-
const settings = getEffectiveSettings(roomName);
276-
277273
const {
278274
deleteMessage,
279275
update: updateMessageRemote,
@@ -381,10 +377,10 @@ export const ChatWindow = ({
381377
}}
382378
hasMoreHistory={hasMoreHistory}
383379
enableTypingIndicators={enableTypingIndicators}
384-
onEdit={settings.allowMessageUpdates ? handleMessageUpdate : undefined}
385-
onDelete={settings.allowMessageDeletes ? handleMessageDelete : undefined}
386-
onReactionAdd={settings.allowMessageReactions ? handleReactionAdd : undefined}
387-
onReactionRemove={settings.allowMessageReactions ? handleReactionRemove : undefined}
380+
onEdit={handleMessageUpdate}
381+
onDelete={handleMessageDelete}
382+
onReactionAdd={handleReactionAdd}
383+
onReactionRemove={handleReactionRemove}
388384
onMessageInView={showMessagesAroundSerial}
389385
onViewLatest={showLatestMessages}
390386
></ChatMessageList>

src/components/molecules/message-actions.tsx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useRoom } from '@ably/chat/react';
12
import React from 'react';
23

4+
import { useChatSettings } from '../../hooks/use-chat-settings.tsx';
35
import { Button } from '../atoms/button.tsx';
46
import { Icon } from '../atoms/icon.tsx';
57

@@ -27,8 +29,8 @@ export interface MessageActionsProps {
2729
* Callback function triggered when the edit button is clicked.
2830
* Should initiate edit mode for the message, typically replacing the message
2931
* content with an editable input field or editor component.
30-
* Only displayed when isOwn is true.
31-
*
32+
* Displayed when:
33+
* - The message is owned by the current user and allowMessageUpdatesOwn is true, or allowMessageUpdatesAny is true
3234
*
3335
* @example
3436
* ```tsx
@@ -44,8 +46,8 @@ export interface MessageActionsProps {
4446
/**
4547
* Callback function triggered when the delete button is clicked.
4648
* Should handle message deletion, typically with confirmation dialog.
47-
* Only displayed when isOwn is true.
48-
*
49+
* Displayed when:
50+
* - The message is owned by the current user and allowMessageDeletesOwn is true, or allowMessageDeletesAny is true
4951
*
5052
* @example
5153
* ```tsx
@@ -59,17 +61,12 @@ export interface MessageActionsProps {
5961

6062
/**
6163
* Whether the message belongs to the current user.
62-
* Determines if edit and delete buttons are shown.
63-
* When false, only the reaction button is displayed.
64-
*
65-
* - Own messages: Show all actions (reaction, edit, delete)
66-
* - Other messages: Show only reaction button
64+
* Used in combination with chat settings to determine if edit and delete buttons are shown.
6765
*
6866
* @example
6967
* ```tsx
7068
* // Basic ownership check
7169
* isOwn={message.senderId === currentUser.id}
72-
*
7370
* ```
7471
*/
7572
isOwn: boolean;
@@ -116,10 +113,35 @@ export const MessageActions = ({
116113
onDeleteButtonClicked,
117114
isOwn,
118115
}: MessageActionsProps) => {
119-
// Check if there are any actions to display
120-
const hasReactionAction = onReactionButtonClicked !== undefined;
121-
const hasEditAction = isOwn && onEditButtonClicked !== undefined;
122-
const hasDeleteAction = isOwn && onDeleteButtonClicked !== undefined;
116+
// Get the current room name
117+
const { roomName } = useRoom();
118+
119+
// Get chat settings for the current room
120+
const { getEffectiveSettings } = useChatSettings();
121+
const settings = getEffectiveSettings(roomName);
122+
123+
const {
124+
allowMessageUpdatesOwn,
125+
allowMessageUpdatesAny,
126+
allowMessageDeletesOwn,
127+
allowMessageDeletesAny,
128+
allowMessageReactions,
129+
} = settings;
130+
131+
// Check if there are any actions to display based on settings and permissions
132+
const hasReactionAction = allowMessageReactions && onReactionButtonClicked !== undefined;
133+
134+
// Can edit if:
135+
// - User owns the message AND can edit own messages, OR
136+
// - User can edit any message
137+
const canEdit = (isOwn && allowMessageUpdatesOwn) || allowMessageUpdatesAny;
138+
const hasEditAction = canEdit && onEditButtonClicked !== undefined;
139+
140+
// Can delete if:
141+
// - User owns the message AND can delete own messages, OR
142+
// - User can delete any message
143+
const canDelete = (isOwn && allowMessageDeletesOwn) || allowMessageDeletesAny;
144+
const hasDeleteAction = canDelete && onDeleteButtonClicked !== undefined;
123145

124146
// If no actions are available, don't render anything
125147
if (!hasReactionAction && !hasEditAction && !hasDeleteAction) {
@@ -128,7 +150,7 @@ export const MessageActions = ({
128150

129151
return (
130152
<div
131-
className="absolute -top-10 right-0 z-10 flex items-center gap-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-md p-1"
153+
className="absolute -top-9 right-0 z-10 flex items-center gap-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-md p-1"
132154
role="toolbar"
133155
aria-label="Message actions"
134156
>

src/context/chat-settings-context.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { createContext } from 'react';
22

3+
/**
4+
* Interface defining chat feature settings that control UI behavior
5+
*/
36
export interface ChatSettings {
4-
/** Whether users can update their messages after sending */
5-
allowMessageUpdates: boolean;
6-
/** Whether users can delete their messages */
7-
allowMessageDeletes: boolean;
7+
/** Whether users can update their own messages after sending */
8+
allowMessageUpdatesOwn: boolean;
9+
/** Whether users can update any message (not just their own) */
10+
allowMessageUpdatesAny: boolean;
11+
/** Whether users can delete their own messages */
12+
allowMessageDeletesOwn: boolean;
13+
/** Whether users can delete any message (not just their own) */
14+
allowMessageDeletesAny: boolean;
815
/** Whether users can add reactions to messages */
916
allowMessageReactions: boolean;
1017
}

src/providers/avatar-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const useAvatarGeneration = (customColors?: string[]) => {
136136
(text: string): string => {
137137
let hash = 0;
138138
for (let i = 0; i < text.length; i++) {
139-
hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xFFFFFFFF;
139+
hash = ((hash << 5) - hash + (text.codePointAt(i) ?? 0)) & 0xffffffff;
140140
}
141141
return avatarColors[Math.abs(hash) % avatarColors.length] || 'bg-gray-500';
142142
},

src/providers/chat-settings-provider.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
} from '../context/chat-settings-context.tsx';
88

99
export const DEFAULT_SETTINGS: ChatSettings = {
10-
allowMessageUpdates: true,
11-
allowMessageDeletes: true,
10+
allowMessageUpdatesOwn: true,
11+
allowMessageUpdatesAny: false,
12+
allowMessageDeletesOwn: true,
13+
allowMessageDeletesAny: false,
1214
allowMessageReactions: true,
1315
};
1416

@@ -40,20 +42,26 @@ export interface ChatSettingsProviderProps {
4042
* but do not affect the underlying Ably Chat functionality. If you wish to ensure no user can edit or delete messages,
4143
* you must also configure the Ably client capabilities accordingly.
4244
*
43-
*
4445
* @example
4546
* ```tsx
4647
* const globalSettings = {
47-
* allowMessageUpdates: false,
48-
* allowMessageDeletes: true,
48+
* allowMessageUpdatesOwn: true,
49+
* allowMessageUpdatesAny: false,
50+
* allowMessageDeletesOwn: true,
51+
* allowMessageDeletesAny: false,
4952
* allowMessageReactions: true
5053
* };
5154
*
5255
* const roomSettings = {
53-
* 'general': { allowMessageUpdates: true },
56+
* 'general': {
57+
* allowMessageUpdatesOwn: true,
58+
* allowMessageUpdatesAny: true // Allow user to update any message in general room
59+
* },
5460
* 'announcements': {
55-
* allowMessageUpdates: false,
56-
* allowMessageDeletes: false
61+
* allowMessageUpdatesOwn: false,
62+
* allowMessageUpdatesAny: false,
63+
* allowMessageDeletesOwn: false,
64+
* allowMessageDeletesAny: true // Allow user to delete any messages in announcements
5765
* }
5866
* };
5967
*

src/stories/chat-message-list.stories.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MockChatClient,
1212
} from '../../.storybook/mocks/mock-ably-chat.ts';
1313
import { AvatarProvider } from '../providers/avatar-provider.tsx';
14+
import { ChatSettingsProvider } from '../providers';
1415

1516
const messages = [
1617
createMockMessage({
@@ -61,15 +62,20 @@ const meta: Meta<StoryProps> = {
6162
component: ChatMessageList,
6263
decorators: [
6364
(Story, context) => (
64-
<ChatClientProvider client={new MockChatClient()} mockOverrides={context.args.mockOverrides}>
65-
<AvatarProvider>
66-
<div className="h-screen w-full flex items-center justify-center bg-gray-50 dark:bg-gray-950">
67-
<div className="h-full max-w-xl w-full border rounded-md overflow-hidden bg-white dark:bg-gray-900 flex flex-col">
68-
<Story />
65+
<ChatSettingsProvider>
66+
<ChatClientProvider
67+
client={new MockChatClient()}
68+
mockOverrides={context.args.mockOverrides}
69+
>
70+
<AvatarProvider>
71+
<div className="h-screen w-full flex items-center justify-center bg-gray-50 dark:bg-gray-950">
72+
<div className="h-full max-w-xl w-full border rounded-md overflow-hidden bg-white dark:bg-gray-900 flex flex-col">
73+
<Story />
74+
</div>
6975
</div>
70-
</div>
71-
</AvatarProvider>
72-
</ChatClientProvider>
76+
</AvatarProvider>
77+
</ChatClientProvider>
78+
</ChatSettingsProvider>
7379
),
7480
],
7581
parameters: {

0 commit comments

Comments
 (0)