stream-chat-react-native-core 9.4.0-beta.6 → 9.4.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/lib/commonjs/components/Attachment/Attachment.js +4 -3
  2. package/lib/commonjs/components/Attachment/Attachment.js.map +1 -1
  3. package/lib/commonjs/components/ChannelDetails/hooks/useChannelDetailsActionItems.js +2 -1
  4. package/lib/commonjs/components/ChannelDetails/hooks/useChannelDetailsActionItems.js.map +1 -1
  5. package/lib/commonjs/components/ChannelList/ChannelList.js +4 -1
  6. package/lib/commonjs/components/ChannelList/ChannelList.js.map +1 -1
  7. package/lib/commonjs/components/ChannelList/hooks/useCreateChannelsContext.js +5 -3
  8. package/lib/commonjs/components/ChannelList/hooks/useCreateChannelsContext.js.map +1 -1
  9. package/lib/commonjs/components/ChannelPreview/ChannelPreview.js +3 -0
  10. package/lib/commonjs/components/ChannelPreview/ChannelPreview.js.map +1 -1
  11. package/lib/commonjs/components/ChannelPreview/ChannelPreviewPinnedStatus.js +30 -0
  12. package/lib/commonjs/components/ChannelPreview/ChannelPreviewPinnedStatus.js.map +1 -0
  13. package/lib/commonjs/components/ChannelPreview/ChannelPreviewView.js +8 -3
  14. package/lib/commonjs/components/ChannelPreview/ChannelPreviewView.js.map +1 -1
  15. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js +2 -1
  16. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  17. package/lib/commonjs/components/ChannelPreview/hooks/index.js +11 -0
  18. package/lib/commonjs/components/ChannelPreview/hooks/index.js.map +1 -1
  19. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewData.js +3 -0
  20. package/lib/commonjs/components/ChannelPreview/hooks/useChannelPreviewData.js.map +1 -1
  21. package/lib/commonjs/components/ChannelPreview/hooks/useIsChannelPinned.js +11 -0
  22. package/lib/commonjs/components/ChannelPreview/hooks/useIsChannelPinned.js.map +1 -0
  23. package/lib/commonjs/components/Poll/components/PollModalHeader.js +1 -2
  24. package/lib/commonjs/components/Poll/components/PollModalHeader.js.map +1 -1
  25. package/lib/commonjs/components/index.js +11 -0
  26. package/lib/commonjs/components/index.js.map +1 -1
  27. package/lib/commonjs/contexts/channelsContext/ChannelsContext.js.map +1 -1
  28. package/lib/commonjs/contexts/componentsContext/defaultComponents.js +2 -0
  29. package/lib/commonjs/contexts/componentsContext/defaultComponents.js.map +1 -1
  30. package/lib/commonjs/contexts/themeContext/utils/theme.js +1 -0
  31. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  32. package/lib/commonjs/hooks/actions/useChannelActionItems.js +20 -1
  33. package/lib/commonjs/hooks/actions/useChannelActionItems.js.map +1 -1
  34. package/lib/commonjs/hooks/actions/useChannelActionItemsById.js +3 -1
  35. package/lib/commonjs/hooks/actions/useChannelActionItemsById.js.map +1 -1
  36. package/lib/commonjs/i18n/ar.json +6 -1
  37. package/lib/commonjs/i18n/en.json +5 -0
  38. package/lib/commonjs/i18n/es.json +6 -1
  39. package/lib/commonjs/i18n/fr.json +6 -1
  40. package/lib/commonjs/i18n/he.json +6 -1
  41. package/lib/commonjs/i18n/hi.json +6 -1
  42. package/lib/commonjs/i18n/it.json +6 -1
  43. package/lib/commonjs/i18n/ja.json +6 -1
  44. package/lib/commonjs/i18n/ko.json +6 -1
  45. package/lib/commonjs/i18n/nl.json +6 -1
  46. package/lib/commonjs/i18n/pt-br.json +6 -1
  47. package/lib/commonjs/i18n/ru.json +6 -1
  48. package/lib/commonjs/i18n/tr.json +6 -1
  49. package/lib/commonjs/icons/index.js +12 -0
  50. package/lib/commonjs/icons/index.js.map +1 -1
  51. package/lib/commonjs/version.json +1 -1
  52. package/lib/module/components/Attachment/Attachment.js +4 -3
  53. package/lib/module/components/Attachment/Attachment.js.map +1 -1
  54. package/lib/module/components/ChannelDetails/hooks/useChannelDetailsActionItems.js +2 -1
  55. package/lib/module/components/ChannelDetails/hooks/useChannelDetailsActionItems.js.map +1 -1
  56. package/lib/module/components/ChannelList/ChannelList.js +4 -1
  57. package/lib/module/components/ChannelList/ChannelList.js.map +1 -1
  58. package/lib/module/components/ChannelList/hooks/useCreateChannelsContext.js +5 -3
  59. package/lib/module/components/ChannelList/hooks/useCreateChannelsContext.js.map +1 -1
  60. package/lib/module/components/ChannelPreview/ChannelPreview.js +3 -0
  61. package/lib/module/components/ChannelPreview/ChannelPreview.js.map +1 -1
  62. package/lib/module/components/ChannelPreview/ChannelPreviewPinnedStatus.js +30 -0
  63. package/lib/module/components/ChannelPreview/ChannelPreviewPinnedStatus.js.map +1 -0
  64. package/lib/module/components/ChannelPreview/ChannelPreviewView.js +8 -3
  65. package/lib/module/components/ChannelPreview/ChannelPreviewView.js.map +1 -1
  66. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js +2 -1
  67. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  68. package/lib/module/components/ChannelPreview/hooks/index.js +11 -0
  69. package/lib/module/components/ChannelPreview/hooks/index.js.map +1 -1
  70. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewData.js +3 -0
  71. package/lib/module/components/ChannelPreview/hooks/useChannelPreviewData.js.map +1 -1
  72. package/lib/module/components/ChannelPreview/hooks/useIsChannelPinned.js +11 -0
  73. package/lib/module/components/ChannelPreview/hooks/useIsChannelPinned.js.map +1 -0
  74. package/lib/module/components/Poll/components/PollModalHeader.js +1 -2
  75. package/lib/module/components/Poll/components/PollModalHeader.js.map +1 -1
  76. package/lib/module/components/index.js +11 -0
  77. package/lib/module/components/index.js.map +1 -1
  78. package/lib/module/contexts/channelsContext/ChannelsContext.js.map +1 -1
  79. package/lib/module/contexts/componentsContext/defaultComponents.js +2 -0
  80. package/lib/module/contexts/componentsContext/defaultComponents.js.map +1 -1
  81. package/lib/module/contexts/themeContext/utils/theme.js +1 -0
  82. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  83. package/lib/module/hooks/actions/useChannelActionItems.js +20 -1
  84. package/lib/module/hooks/actions/useChannelActionItems.js.map +1 -1
  85. package/lib/module/hooks/actions/useChannelActionItemsById.js +3 -1
  86. package/lib/module/hooks/actions/useChannelActionItemsById.js.map +1 -1
  87. package/lib/module/i18n/ar.json +6 -1
  88. package/lib/module/i18n/en.json +5 -0
  89. package/lib/module/i18n/es.json +6 -1
  90. package/lib/module/i18n/fr.json +6 -1
  91. package/lib/module/i18n/he.json +6 -1
  92. package/lib/module/i18n/hi.json +6 -1
  93. package/lib/module/i18n/it.json +6 -1
  94. package/lib/module/i18n/ja.json +6 -1
  95. package/lib/module/i18n/ko.json +6 -1
  96. package/lib/module/i18n/nl.json +6 -1
  97. package/lib/module/i18n/pt-br.json +6 -1
  98. package/lib/module/i18n/ru.json +6 -1
  99. package/lib/module/i18n/tr.json +6 -1
  100. package/lib/module/icons/index.js +12 -0
  101. package/lib/module/icons/index.js.map +1 -1
  102. package/lib/module/version.json +1 -1
  103. package/lib/typescript/components/ChannelList/ChannelList.d.ts +1 -1
  104. package/lib/typescript/components/ChannelList/ChannelList.d.ts.map +1 -1
  105. package/lib/typescript/components/ChannelList/hooks/useCreateChannelsContext.d.ts +1 -1
  106. package/lib/typescript/components/ChannelList/hooks/useCreateChannelsContext.d.ts.map +1 -1
  107. package/lib/typescript/components/ChannelPreview/ChannelPreview.d.ts.map +1 -1
  108. package/lib/typescript/components/ChannelPreview/ChannelPreviewPinnedStatus.d.ts +6 -0
  109. package/lib/typescript/components/ChannelPreview/ChannelPreviewPinnedStatus.d.ts.map +1 -0
  110. package/lib/typescript/components/ChannelPreview/ChannelPreviewView.d.ts +3 -1
  111. package/lib/typescript/components/ChannelPreview/ChannelPreviewView.d.ts.map +1 -1
  112. package/lib/typescript/components/ChannelPreview/ChannelSwipableWrapper.d.ts.map +1 -1
  113. package/lib/typescript/components/ChannelPreview/hooks/index.d.ts +1 -0
  114. package/lib/typescript/components/ChannelPreview/hooks/index.d.ts.map +1 -1
  115. package/lib/typescript/components/ChannelPreview/hooks/useChannelPreviewData.d.ts +1 -0
  116. package/lib/typescript/components/ChannelPreview/hooks/useChannelPreviewData.d.ts.map +1 -1
  117. package/lib/typescript/components/ChannelPreview/hooks/useIsChannelPinned.d.ts +3 -0
  118. package/lib/typescript/components/ChannelPreview/hooks/useIsChannelPinned.d.ts.map +1 -0
  119. package/lib/typescript/components/index.d.ts +1 -0
  120. package/lib/typescript/components/index.d.ts.map +1 -1
  121. package/lib/typescript/contexts/channelsContext/ChannelsContext.d.ts +1 -0
  122. package/lib/typescript/contexts/channelsContext/ChannelsContext.d.ts.map +1 -1
  123. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts +1 -0
  124. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts.map +1 -1
  125. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts +1 -0
  126. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts.map +1 -1
  127. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +1 -0
  128. package/lib/typescript/contexts/themeContext/utils/theme.d.ts.map +1 -1
  129. package/lib/typescript/hooks/actions/useChannelActionItems.d.ts +35 -2
  130. package/lib/typescript/hooks/actions/useChannelActionItems.d.ts.map +1 -1
  131. package/lib/typescript/hooks/actions/useChannelActionItemsById.d.ts +3 -2
  132. package/lib/typescript/hooks/actions/useChannelActionItemsById.d.ts.map +1 -1
  133. package/lib/typescript/i18n/ar.json +6 -1
  134. package/lib/typescript/i18n/en.json +5 -0
  135. package/lib/typescript/i18n/es.json +6 -1
  136. package/lib/typescript/i18n/fr.json +6 -1
  137. package/lib/typescript/i18n/he.json +6 -1
  138. package/lib/typescript/i18n/hi.json +6 -1
  139. package/lib/typescript/i18n/it.json +6 -1
  140. package/lib/typescript/i18n/ja.json +6 -1
  141. package/lib/typescript/i18n/ko.json +6 -1
  142. package/lib/typescript/i18n/nl.json +6 -1
  143. package/lib/typescript/i18n/pt-br.json +6 -1
  144. package/lib/typescript/i18n/ru.json +6 -1
  145. package/lib/typescript/i18n/tr.json +6 -1
  146. package/lib/typescript/icons/index.d.ts +1 -0
  147. package/lib/typescript/icons/index.d.ts.map +1 -1
  148. package/lib/typescript/utils/i18n/Streami18n.d.ts +5 -0
  149. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  150. package/package.json +1 -1
  151. package/src/components/Attachment/Attachment.tsx +8 -3
  152. package/src/components/ChannelDetails/__tests__/useChannelDetailsActionItems.test.tsx +6 -2
  153. package/src/components/ChannelDetails/hooks/useChannelDetailsActionItems.ts +1 -1
  154. package/src/components/ChannelList/ChannelList.tsx +3 -0
  155. package/src/components/ChannelList/hooks/useCreateChannelsContext.ts +3 -0
  156. package/src/components/ChannelPreview/ChannelPreview.tsx +21 -3
  157. package/src/components/ChannelPreview/ChannelPreviewPinnedStatus.tsx +25 -0
  158. package/src/components/ChannelPreview/ChannelPreviewView.tsx +18 -2
  159. package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx +5 -1
  160. package/src/components/ChannelPreview/hooks/__tests__/useIsChannelPinned.test.tsx +35 -0
  161. package/src/components/ChannelPreview/hooks/index.ts +1 -0
  162. package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +3 -1
  163. package/src/components/ChannelPreview/hooks/useIsChannelPinned.ts +8 -0
  164. package/src/components/Poll/components/PollModalHeader.tsx +1 -2
  165. package/src/components/index.ts +1 -0
  166. package/src/contexts/channelsContext/ChannelsContext.tsx +1 -0
  167. package/src/contexts/componentsContext/defaultComponents.ts +2 -0
  168. package/src/contexts/themeContext/utils/theme.ts +2 -0
  169. package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx +137 -14
  170. package/src/hooks/actions/__tests__/useChannelActionItemsById.test.tsx +3 -1
  171. package/src/hooks/actions/useChannelActionItems.tsx +60 -2
  172. package/src/hooks/actions/useChannelActionItemsById.ts +8 -1
  173. package/src/i18n/ar.json +6 -1
  174. package/src/i18n/en.json +5 -0
  175. package/src/i18n/es.json +6 -1
  176. package/src/i18n/fr.json +6 -1
  177. package/src/i18n/he.json +6 -1
  178. package/src/i18n/hi.json +6 -1
  179. package/src/i18n/it.json +6 -1
  180. package/src/i18n/ja.json +6 -1
  181. package/src/i18n/ko.json +6 -1
  182. package/src/i18n/nl.json +6 -1
  183. package/src/i18n/pt-br.json +6 -1
  184. package/src/i18n/ru.json +6 -1
  185. package/src/i18n/tr.json +6 -1
  186. package/src/icons/index.ts +1 -0
  187. package/src/version.json +1 -1
@@ -4,6 +4,7 @@ import throttle from 'lodash/throttle';
4
4
  import type { Channel, Event, LocalMessage, MessageResponse, StreamChat } from 'stream-chat';
5
5
 
6
6
  import { useIsChannelMuted } from './useIsChannelMuted';
7
+ import { useIsChannelPinned } from './useIsChannelPinned';
7
8
 
8
9
  import { useChannelsContext } from '../../../contexts';
9
10
  import { useStableCallback } from '../../../hooks';
@@ -39,6 +40,7 @@ export const useChannelPreviewData = (
39
40
  );
40
41
  const [unread, setUnread] = useState(channel.countUnread());
41
42
  const { muted } = useIsChannelMuted(channel);
43
+ const pinned = useIsChannelPinned(channel);
42
44
  const { forceUpdate: contextForceUpdate } = useChannelsContext();
43
45
  const channelListForceUpdate = forceUpdateOverride ?? contextForceUpdate;
44
46
 
@@ -170,5 +172,5 @@ export const useChannelPreviewData = (
170
172
  return () => listeners.forEach((l) => l.unsubscribe());
171
173
  }, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate, setLastMessage]);
172
174
 
173
- return { lastMessage, muted, unread };
175
+ return { lastMessage, muted, pinned, unread };
174
176
  };
@@ -0,0 +1,8 @@
1
+ import type { Channel } from 'stream-chat';
2
+
3
+ import { useChannelMembershipState } from '../../../hooks/useChannelMembershipState';
4
+
5
+ export const useIsChannelPinned = (channel: Channel) => {
6
+ const membership = useChannelMembershipState(channel);
7
+ return Boolean(membership?.pinned_at);
8
+ };
@@ -56,8 +56,7 @@ const useStyles = () => {
56
56
  alignItems: 'center',
57
57
  flexDirection: 'row',
58
58
  justifyContent: 'space-between',
59
- paddingHorizontal: primitives.spacingMd,
60
- paddingVertical: 10,
59
+ padding: primitives.spacingMd,
61
60
  backgroundColor: semantics.backgroundCoreElevation1,
62
61
  },
63
62
  centerContainer: {
@@ -56,6 +56,7 @@ export * from './ChannelPreview/ChannelPreview';
56
56
  export * from './ChannelPreview/ChannelPreviewMessage';
57
57
  export * from './ChannelPreview/ChannelPreviewView';
58
58
  export * from './ChannelPreview/ChannelPreviewMutedStatus';
59
+ export * from './ChannelPreview/ChannelPreviewPinnedStatus';
59
60
  export * from './ChannelPreview/ChannelLastMessagePreview';
60
61
  export * from './ChannelPreview/ChannelPreviewStatus';
61
62
  export * from './ChannelPreview/ChannelPreviewTitle';
@@ -118,6 +118,7 @@ export type ChannelsContextValue = {
118
118
  swipeActionsEnabled?: boolean;
119
119
 
120
120
  mutedStatusPosition?: 'trailingBottom' | 'inlineTitle';
121
+ pinnedStatusPosition?: 'trailingBottom' | 'inlineTitle';
121
122
  };
122
123
 
123
124
  export const ChannelsContext = React.createContext(
@@ -53,6 +53,7 @@ import { ChannelLastMessagePreview } from '../../components/ChannelPreview/Chann
53
53
  import { ChannelMessagePreviewDeliveryStatus } from '../../components/ChannelPreview/ChannelMessagePreviewDeliveryStatus';
54
54
  import { ChannelPreviewMessage } from '../../components/ChannelPreview/ChannelPreviewMessage';
55
55
  import { ChannelPreviewMutedStatus } from '../../components/ChannelPreview/ChannelPreviewMutedStatus';
56
+ import { ChannelPreviewPinnedStatus } from '../../components/ChannelPreview/ChannelPreviewPinnedStatus';
56
57
  import { ChannelPreviewStatus } from '../../components/ChannelPreview/ChannelPreviewStatus';
57
58
  import { ChannelPreviewTitle } from '../../components/ChannelPreview/ChannelPreviewTitle';
58
59
  import { ChannelPreviewTypingIndicator } from '../../components/ChannelPreview/ChannelPreviewTypingIndicator';
@@ -271,6 +272,7 @@ const components = {
271
272
  ChannelPreviewMessage,
272
273
  ChannelPreviewMessageDeliveryStatus: ChannelMessagePreviewDeliveryStatus,
273
274
  ChannelPreviewMutedStatus,
275
+ ChannelPreviewPinnedStatus,
274
276
  ChannelPreviewStatus,
275
277
  ChannelPreviewTitle,
276
278
  ChannelPreviewTypingIndicator,
@@ -299,6 +299,7 @@ export type Theme = {
299
299
  contentContainer: ViewStyle;
300
300
  date: TextStyle;
301
301
  mutedStatus: IconProps;
302
+ pinnedStatus: IconProps;
302
303
  messageDeliveryStatus: {
303
304
  container: ViewStyle;
304
305
  text: TextStyle;
@@ -1357,6 +1358,7 @@ export const defaultTheme: Theme = {
1357
1358
  timeIcon: {},
1358
1359
  },
1359
1360
  mutedStatus: {},
1361
+ pinnedStatus: {},
1360
1362
  lowerRow: {},
1361
1363
  title: {},
1362
1364
  unreadContainer: {},
@@ -99,32 +99,45 @@ describe('useChannelActionItems', () => {
99
99
  });
100
100
 
101
101
  it('returns default channel action items', () => {
102
- const { result } = renderHook(() => useChannelActionItems({ channel }));
102
+ const { result } = renderHook(() => useChannelActionItems({ channel, surface: 'list' }));
103
103
 
104
- expect(result.current).toHaveLength(3);
104
+ expect(result.current).toHaveLength(4);
105
105
  expect(result.current.map((item) => item.action)).toEqual([
106
106
  channelActions.muteChannel,
107
+ channelActions.pin,
107
108
  channelActions.leave,
108
109
  expect.any(Function),
109
110
  ]);
110
- expect(result.current.map((item) => item.id)).toEqual(['mute', 'leave', 'deleteChannel']);
111
+ expect(result.current.map((item) => item.id)).toEqual([
112
+ 'mute',
113
+ 'pin',
114
+ 'leave',
115
+ 'deleteChannel',
116
+ ]);
111
117
  expect(result.current.map((item) => item.type)).toEqual([
118
+ 'standard',
112
119
  'standard',
113
120
  'destructive',
114
121
  'destructive',
115
122
  ]);
116
- expect(result.current.map((item) => item.placement)).toEqual(['swipe', 'sheet', 'sheet']);
123
+ expect(result.current.map((item) => item.placement)).toEqual([
124
+ 'swipe',
125
+ 'sheet',
126
+ 'sheet',
127
+ 'sheet',
128
+ ]);
117
129
  });
118
130
 
119
131
  it('returns muteUser only in direct chats', () => {
120
132
  jest.spyOn(useIsDirectChatModule, 'useIsDirectChat').mockReturnValue(true);
121
133
 
122
- const { result } = renderHook(() => useChannelActionItems({ channel }));
134
+ const { result } = renderHook(() => useChannelActionItems({ channel, surface: 'list' }));
123
135
 
124
136
  expect(result.current.map((item) => item.id)).toEqual([
125
137
  'mute',
126
138
  'muteUser',
127
139
  'block',
140
+ 'pin',
128
141
  'leave',
129
142
  'deleteChannel',
130
143
  ]);
@@ -143,7 +156,7 @@ describe('useChannelActionItems', () => {
143
156
  });
144
157
  jest.spyOn(useMutedUsersModule, 'useMutedUsers').mockReturnValue([] as Mute[]);
145
158
 
146
- const { result } = renderHook(() => useChannelActionItems({ channel }));
159
+ const { result } = renderHook(() => useChannelActionItems({ channel, surface: 'list' }));
147
160
 
148
161
  const muteItem = result.current.find((item) => item.id === 'mute');
149
162
  const muteUserItem = result.current.find((item) => item.id === 'muteUser');
@@ -152,7 +165,7 @@ describe('useChannelActionItems', () => {
152
165
  });
153
166
 
154
167
  it('forwards options from item.action to the underlying channel action', async () => {
155
- const { result } = renderHook(() => useChannelActionItems({ channel }));
168
+ const { result } = renderHook(() => useChannelActionItems({ channel, surface: 'list' }));
156
169
 
157
170
  const muteItem = result.current.find((item) => item.id === 'mute');
158
171
  expect(muteItem).toBeDefined();
@@ -165,7 +178,7 @@ describe('useChannelActionItems', () => {
165
178
  it('marks block as destructive when user is not blocked', () => {
166
179
  jest.spyOn(useIsDirectChatModule, 'useIsDirectChat').mockReturnValue(true);
167
180
 
168
- const { result } = renderHook(() => useChannelActionItems({ channel }));
181
+ const { result } = renderHook(() => useChannelActionItems({ channel, surface: 'list' }));
169
182
 
170
183
  const blockItem = result.current.find((item) => item.id === 'block');
171
184
  expect(blockItem?.type).toBe('destructive');
@@ -175,7 +188,9 @@ describe('useChannelActionItems', () => {
175
188
  jest.spyOn(useIsDirectChatModule, 'useIsDirectChat').mockReturnValue(true);
176
189
  const blockedChannel = createChannelMock({ blockedUserIds: ['other-user-id'] });
177
190
 
178
- const { result } = renderHook(() => useChannelActionItems({ channel: blockedChannel }));
191
+ const { result } = renderHook(() =>
192
+ useChannelActionItems({ channel: blockedChannel, surface: 'list' }),
193
+ );
179
194
 
180
195
  const blockItem = result.current.find((item) => item.id === 'block');
181
196
  expect(blockItem?.type).toBe('standard');
@@ -190,6 +205,7 @@ describe('useChannelActionItems', () => {
190
205
  useChannelActionItems({
191
206
  channel,
192
207
  getChannelActionItems: customGetChannelActionItems,
208
+ surface: 'list',
193
209
  }),
194
210
  );
195
211
 
@@ -202,6 +218,7 @@ describe('useChannelActionItems', () => {
202
218
  isBlocked: undefined,
203
219
  isDirectChat: false,
204
220
  isPinned: false,
221
+ surface: 'list',
205
222
  t: expect.any(Function),
206
223
  userMuteActive: false,
207
224
  },
@@ -228,6 +245,7 @@ describe('getChannelActionItems', () => {
228
245
  isBlocked: undefined,
229
246
  isDirectChat: false,
230
247
  isPinned: false,
248
+ surface: 'list',
231
249
  t: ((value: string) => value) as TranslationContextValue['t'],
232
250
  userMuteActive: false,
233
251
  });
@@ -240,6 +258,7 @@ describe('getChannelActionItems', () => {
240
258
  isBlocked: undefined,
241
259
  isDirectChat: false,
242
260
  isPinned: false,
261
+ surface: 'list',
243
262
  t: ((value: string) => value) as TranslationContextValue['t'],
244
263
  userMuteActive: false,
245
264
  },
@@ -248,11 +267,13 @@ describe('getChannelActionItems', () => {
248
267
 
249
268
  expect(actionItems.map((item) => item.action)).toEqual([
250
269
  channelActions.muteChannel,
270
+ channelActions.pin,
251
271
  channelActions.leave,
252
272
  expect.any(Function),
253
273
  ]);
254
- expect(actionItems.map((item) => item.id)).toEqual(['mute', 'leave', 'deleteChannel']);
274
+ expect(actionItems.map((item) => item.id)).toEqual(['mute', 'pin', 'leave', 'deleteChannel']);
255
275
  expect(actionItems.map((item) => item.type)).toEqual([
276
+ 'standard',
256
277
  'standard',
257
278
  'destructive',
258
279
  'destructive',
@@ -269,6 +290,7 @@ describe('getChannelActionItems', () => {
269
290
  isBlocked: true,
270
291
  isDirectChat: true,
271
292
  isPinned: false,
293
+ surface: 'list',
272
294
  t: ((value: string) => value) as TranslationContextValue['t'],
273
295
  userMuteActive: true,
274
296
  });
@@ -277,6 +299,7 @@ describe('getChannelActionItems', () => {
277
299
  'mute',
278
300
  'muteUser',
279
301
  'block',
302
+ 'pin',
280
303
  'leave',
281
304
  'deleteChannel',
282
305
  ]);
@@ -284,6 +307,7 @@ describe('getChannelActionItems', () => {
284
307
  channelActions.unmuteChannel,
285
308
  channelActions.unmuteUser,
286
309
  channelActions.unblockUser,
310
+ channelActions.pin,
287
311
  channelActions.leave,
288
312
  expect.any(Function),
289
313
  ]);
@@ -291,6 +315,7 @@ describe('getChannelActionItems', () => {
291
315
  'Unmute Chat',
292
316
  'Unmute User',
293
317
  'Unblock User',
318
+ 'Pin Chat',
294
319
  'Leave Chat',
295
320
  'Delete Chat',
296
321
  ]);
@@ -300,6 +325,7 @@ describe('getChannelActionItems', () => {
300
325
  'sheet',
301
326
  'sheet',
302
327
  'sheet',
328
+ 'sheet',
303
329
  ]);
304
330
  });
305
331
 
@@ -312,6 +338,7 @@ describe('getChannelActionItems', () => {
312
338
  isBlocked: undefined,
313
339
  isDirectChat: false,
314
340
  isPinned: false,
341
+ surface: 'list',
315
342
  t: ((value: string) => value) as TranslationContextValue['t'],
316
343
  userMuteActive: false,
317
344
  });
@@ -328,11 +355,12 @@ describe('getChannelActionItems', () => {
328
355
  isBlocked: undefined,
329
356
  isDirectChat: false,
330
357
  isPinned: false,
358
+ surface: 'list',
331
359
  t: ((value: string) => value) as TranslationContextValue['t'],
332
360
  userMuteActive: false,
333
361
  });
334
362
 
335
- expect(actionItems.map((item) => item.id)).toEqual(['mute', 'leave']);
363
+ expect(actionItems.map((item) => item.id)).toEqual(['mute', 'pin', 'leave']);
336
364
  });
337
365
 
338
366
  it('uses group mute variants for labels and placements', () => {
@@ -345,6 +373,7 @@ describe('getChannelActionItems', () => {
345
373
  isBlocked: undefined,
346
374
  isDirectChat: false,
347
375
  isPinned: false,
376
+ surface: 'list',
348
377
  t: ((value: string) => value) as TranslationContextValue['t'],
349
378
  userMuteActive: false,
350
379
  });
@@ -353,9 +382,101 @@ describe('getChannelActionItems', () => {
353
382
  expect(actionItems[0].label).toBe('Unmute Group');
354
383
  expect(actionItems[0].placement).toBe('swipe');
355
384
 
356
- expect(actionItems[1].action).toBe(channelActions.leave);
357
- expect(actionItems[1].label).toBe('Leave Group');
358
- expect(actionItems[1].placement).toBe('sheet');
385
+ const leaveItem = actionItems.find((item) => item.id === 'leave');
386
+ expect(leaveItem?.action).toBe(channelActions.leave);
387
+ expect(leaveItem?.label).toBe('Leave Group');
388
+ expect(leaveItem?.placement).toBe('sheet');
389
+ });
390
+
391
+ it('pin item toggles to unpin when channel is pinned', () => {
392
+ const channelActions = createChannelActions();
393
+ const actionItems = buildDefaultChannelActionItems({
394
+ actions: channelActions,
395
+ channel,
396
+ channelMuteActive: false,
397
+ isArchived: false,
398
+ isBlocked: undefined,
399
+ isDirectChat: false,
400
+ isPinned: true,
401
+ surface: 'list',
402
+ t: ((value: string) => value) as TranslationContextValue['t'],
403
+ userMuteActive: false,
404
+ });
405
+
406
+ const pinItem = actionItems.find((item) => item.id === 'pin');
407
+ expect(pinItem?.action).toBe(channelActions.unpin);
408
+ expect(pinItem?.label).toBe('Unpin Group');
409
+ expect(pinItem?.placement).toBe('sheet');
410
+ });
411
+
412
+ it('pin item uses direct-chat label variant', () => {
413
+ const channelActions = createChannelActions();
414
+ const actionItems = buildDefaultChannelActionItems({
415
+ actions: channelActions,
416
+ channel,
417
+ channelMuteActive: false,
418
+ isArchived: false,
419
+ isBlocked: undefined,
420
+ isDirectChat: true,
421
+ isPinned: false,
422
+ surface: 'list',
423
+ t: ((value: string) => value) as TranslationContextValue['t'],
424
+ userMuteActive: false,
425
+ });
426
+
427
+ const pinItem = actionItems.find((item) => item.id === 'pin');
428
+ expect(pinItem?.action).toBe(channelActions.pin);
429
+ expect(pinItem?.label).toBe('Pin Chat');
430
+ });
431
+
432
+ it('omits the pin item when building for the details surface', () => {
433
+ const actionItems = buildDefaultChannelActionItems({
434
+ actions: createChannelActions(),
435
+ channel,
436
+ channelMuteActive: false,
437
+ isArchived: false,
438
+ isBlocked: undefined,
439
+ isDirectChat: false,
440
+ isPinned: false,
441
+ surface: 'details',
442
+ t: ((value: string) => value) as TranslationContextValue['t'],
443
+ userMuteActive: false,
444
+ });
445
+
446
+ expect(actionItems.map((item) => item.id)).not.toContain('pin');
447
+ });
448
+
449
+ it('includes the pin item when building for the list surface', () => {
450
+ const actionItems = buildDefaultChannelActionItems({
451
+ actions: createChannelActions(),
452
+ channel,
453
+ channelMuteActive: false,
454
+ isArchived: false,
455
+ isBlocked: undefined,
456
+ isDirectChat: false,
457
+ isPinned: false,
458
+ surface: 'list',
459
+ t: ((value: string) => value) as TranslationContextValue['t'],
460
+ userMuteActive: false,
461
+ });
462
+
463
+ expect(actionItems.map((item) => item.id)).toContain('pin');
464
+ });
465
+
466
+ it('applies no surface-specific filtering when surface is omitted', () => {
467
+ const actionItems = buildDefaultChannelActionItems({
468
+ actions: createChannelActions(),
469
+ channel,
470
+ channelMuteActive: false,
471
+ isArchived: false,
472
+ isBlocked: undefined,
473
+ isDirectChat: false,
474
+ isPinned: false,
475
+ t: ((value: string) => value) as TranslationContextValue['t'],
476
+ userMuteActive: false,
477
+ });
478
+
479
+ expect(actionItems.map((item) => item.id)).toEqual(['mute', 'pin', 'leave', 'deleteChannel']);
359
480
  });
360
481
 
361
482
  it('mute and muteUser reflect their respective active states independently', () => {
@@ -368,6 +489,7 @@ describe('getChannelActionItems', () => {
368
489
  isBlocked: undefined,
369
490
  isDirectChat: true,
370
491
  isPinned: false,
492
+ surface: 'list',
371
493
  t: ((value: string) => value) as TranslationContextValue['t'],
372
494
  userMuteActive: true,
373
495
  });
@@ -393,6 +515,7 @@ describe('getChannelActionItems', () => {
393
515
  isBlocked: undefined,
394
516
  isDirectChat: false,
395
517
  isPinned: false,
518
+ surface: 'list',
396
519
  t: ((value: string) => value) as TranslationContextValue['t'],
397
520
  userMuteActive: false,
398
521
  });
@@ -41,7 +41,7 @@ describe('useChannelActionItemsById', () => {
41
41
  .spyOn(useChannelActionItemsModule, 'useChannelActionItems')
42
42
  .mockReturnValue(channelActionItems);
43
43
 
44
- const { result } = renderHook(() => useChannelActionItemsById({ channel }));
44
+ const { result } = renderHook(() => useChannelActionItemsById({ channel, surface: 'list' }));
45
45
 
46
46
  expect(result.current.pin).toBe(channelActionItems[0]);
47
47
  expect(result.current.deleteChannel).toBe(channelActionItems[1]);
@@ -59,12 +59,14 @@ describe('useChannelActionItemsById', () => {
59
59
  useChannelActionItemsById({
60
60
  channel,
61
61
  getChannelActionItems,
62
+ surface: 'list',
62
63
  }),
63
64
  );
64
65
 
65
66
  expect(useChannelActionItemsSpy).toHaveBeenCalledWith({
66
67
  channel,
67
68
  getChannelActionItems,
69
+ surface: 'list',
68
70
  });
69
71
  });
70
72
  });
@@ -15,18 +15,50 @@ import { useIsChannelMuted } from '../../components/ChannelPreview/hooks/useIsCh
15
15
  import { useUserMuteActive } from '../../components/Message/hooks/useUserMuteActive';
16
16
  import { useTheme, useTranslationContext } from '../../contexts';
17
17
  import type { TranslationContextValue } from '../../contexts/translationContext/TranslationContext';
18
- import { IconProps, Mute, BlockUser, Delete, Sound } from '../../icons';
18
+ import { IconProps, Mute, BlockUser, Delete, Pin, Sound, Unpin } from '../../icons';
19
19
  import { ArrowBoxLeft } from '../../icons/leave';
20
20
  import { useChannelMembershipState } from '../useChannelMembershipState';
21
21
  import { useIsDirectChat } from '../useIsDirectChat';
22
22
  import { useStateStore } from '../useStateStore';
23
23
 
24
24
  export type ChannelActionItem = ActionItem<
25
- 'mute' | 'muteUser' | 'block' | 'leave' | 'deleteChannel' | string
25
+ 'mute' | 'muteUser' | 'block' | 'leave' | 'deleteChannel' | 'pin' | string
26
26
  > & {
27
+ /**
28
+ * Per item routing **within a channel preview interaction** (swipe row vs
29
+ * the sheet that opens from it). Only meaningful when the items are consumed
30
+ * from `<ChannelSwipableWrapper>`:
31
+ *
32
+ * - `'swipe'`: shown only in the swipe-row chips.
33
+ * - `'sheet'`: shown only in the swipe-triggered options sheet.
34
+ * - `'both'`: shown in both swipe row and sheet.
35
+ *
36
+ * The standalone Channel Details screen does **not** filter by `placement` -
37
+ * use {@link ChannelActionSurface} (`surface`) instead to vary items between
38
+ * the channel list and the Channel Details screen.
39
+ */
27
40
  placement: 'both' | 'sheet' | 'swipe';
28
41
  };
29
42
 
43
+ /**
44
+ * Identifies which top level UI surface is requesting channel action items.
45
+ * Passed verbatim into {@link ChannelActionItemsParams} so the default builder
46
+ * and any integrator supplied `getChannelActionItems` can branch on it - i.e.
47
+ * to drop or relabel an item on a specific surface or to provide an entirely
48
+ * different builder per surface.
49
+ *
50
+ * - `'list'`: anything driven by a ChannelList interaction, the swipe row
51
+ * chips on a channel preview and the bottom sheet that opens from them.
52
+ * Subrouting between the swipe row and the sheet is handled by the per item
53
+ * {@link ChannelActionItem.placement} field.
54
+ * - `'details'`: items for the standalone Channel Details screen.
55
+ *
56
+ * `surface` operates at the call site level (which UI is asking). It is
57
+ * optional and when omitted, the default builder applies no surface specific
58
+ * filtering and returns every item it would otherwise produce.
59
+ */
60
+ export type ChannelActionSurface = 'list' | 'details';
61
+
30
62
  export type ChannelActionItemsParams = {
31
63
  actions: ChannelActions;
32
64
  channel: Channel;
@@ -35,6 +67,7 @@ export type ChannelActionItemsParams = {
35
67
  isBlocked: boolean | undefined;
36
68
  isDirectChat: boolean;
37
69
  isPinned: boolean;
70
+ surface?: ChannelActionSurface;
38
71
  t: TranslationContextValue['t'];
39
72
  userMuteActive: boolean;
40
73
  };
@@ -67,10 +100,14 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
67
100
  unmuteUser,
68
101
  blockUser,
69
102
  unblockUser,
103
+ pin,
104
+ unpin,
70
105
  },
71
106
  channelMuteActive,
72
107
  isBlocked,
73
108
  isDirectChat,
109
+ isPinned,
110
+ surface,
74
111
  userMuteActive,
75
112
  t,
76
113
  channel,
@@ -135,6 +172,23 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
135
172
  });
136
173
  }
137
174
 
175
+ if (surface !== 'details') {
176
+ actionItems.push({
177
+ action: isPinned ? unpin : pin,
178
+ Icon: (props) => <ChannelActionsIcon Icon={isPinned ? Unpin : Pin} {...props} />,
179
+ id: 'pin',
180
+ label: isDirectChat
181
+ ? isPinned
182
+ ? t('Unpin Chat')
183
+ : t('Pin Chat')
184
+ : isPinned
185
+ ? t('Unpin Group')
186
+ : t('Pin Group'),
187
+ placement: 'sheet',
188
+ type: 'standard',
189
+ });
190
+ }
191
+
138
192
  actionItems.push({
139
193
  action: leave,
140
194
  Icon: (props) => <ChannelActionsIcon Icon={ArrowBoxLeft} {...props} />,
@@ -186,6 +240,7 @@ export const getChannelActionItems: GetChannelActionItems = ({ defaultItems }) =
186
240
 
187
241
  type UseChannelActionItemsParams = {
188
242
  channel: Channel;
243
+ surface?: ChannelActionSurface;
189
244
  getChannelActionItems?: GetChannelActionItems;
190
245
  };
191
246
 
@@ -194,6 +249,7 @@ const blockedUsersStateSelector = (state: BlockedUsersState) =>
194
249
 
195
250
  export const useChannelActionItems = ({
196
251
  channel,
252
+ surface,
197
253
  getChannelActionItems: getChannelActionItemsProp = getChannelActionItems,
198
254
  }: UseChannelActionItemsParams) => {
199
255
  const { t } = useTranslationContext();
@@ -225,6 +281,7 @@ export const useChannelActionItems = ({
225
281
  isBlocked,
226
282
  isDirectChat,
227
283
  isPinned,
284
+ surface,
228
285
  t,
229
286
  userMuteActive,
230
287
  }),
@@ -236,6 +293,7 @@ export const useChannelActionItems = ({
236
293
  isBlocked,
237
294
  isDirectChat,
238
295
  isPinned,
296
+ surface,
239
297
  t,
240
298
  userMuteActive,
241
299
  ],
@@ -2,23 +2,30 @@ import { useMemo } from 'react';
2
2
 
3
3
  import type { Channel } from 'stream-chat';
4
4
 
5
- import type { ChannelActionItem, GetChannelActionItems } from './useChannelActionItems';
5
+ import type {
6
+ ChannelActionItem,
7
+ ChannelActionSurface,
8
+ GetChannelActionItems,
9
+ } from './useChannelActionItems';
6
10
  import { useChannelActionItems } from './useChannelActionItems';
7
11
 
8
12
  export type ChannelActionItemsById = Partial<Record<ChannelActionItem['id'], ChannelActionItem>>;
9
13
 
10
14
  type UseChannelActionItemsByIdParams = {
11
15
  channel: Channel;
16
+ surface?: ChannelActionSurface;
12
17
  getChannelActionItems?: GetChannelActionItems;
13
18
  };
14
19
 
15
20
  export const useChannelActionItemsById = ({
16
21
  channel,
22
+ surface,
17
23
  getChannelActionItems,
18
24
  }: UseChannelActionItemsByIdParams) => {
19
25
  const channelActionItems = useChannelActionItems({
20
26
  channel,
21
27
  getChannelActionItems,
28
+ surface,
22
29
  });
23
30
 
24
31
  return useMemo(
package/src/i18n/ar.json CHANGED
@@ -274,6 +274,7 @@
274
274
  "a11y/Loading failed": "Loading failed",
275
275
  "a11y/Message actions": "Message actions",
276
276
  "a11y/Muted": "مكتوم",
277
+ "a11y/Pinned": "مثبت",
277
278
  "a11y/New message from {{user}}": "New message from {{user}}",
278
279
  "a11y/Offline": "Offline",
279
280
  "a11y/Open message actions": "Open message actions",
@@ -435,5 +436,9 @@
435
436
  "a11y/Message from {{sender}}": "رسالة من {{sender}}",
436
437
  "a11y/Gallery Image": "صورة من المعرض",
437
438
  "a11y/Gallery Video": "فيديو من المعرض",
438
- "a11y/{{position}} of {{count}}": "{{position}} من {{count}}"
439
+ "a11y/{{position}} of {{count}}": "{{position}} من {{count}}",
440
+ "Pin Chat": "تثبيت الدردشة",
441
+ "Pin Group": "تثبيت المجموعة",
442
+ "Unpin Chat": "إلغاء تثبيت الدردشة",
443
+ "Unpin Group": "إلغاء تثبيت المجموعة"
439
444
  }
package/src/i18n/en.json CHANGED
@@ -246,6 +246,8 @@
246
246
  "Leave Group": "Leave Group",
247
247
  "Mute Chat": "Mute Chat",
248
248
  "Mute Group": "Mute Group",
249
+ "Pin Chat": "Pin Chat",
250
+ "Pin Group": "Pin Group",
249
251
  "Admin": "Admin",
250
252
  "Moderator": "Moderator",
251
253
  "Offline": "Offline",
@@ -255,6 +257,8 @@
255
257
  "Unarchive Group": "Unarchive Group",
256
258
  "Unmute Chat": "Unmute Chat",
257
259
  "Unmute Group": "Unmute Group",
260
+ "Unpin Chat": "Unpin Chat",
261
+ "Unpin Group": "Unpin Group",
258
262
  "{{memberCount}} members, {{onlineCount}} online_one": "{{memberCount}} member, {{onlineCount}} online",
259
263
  "{{memberCount}} members, {{onlineCount}} online_other": "{{memberCount}} members, {{onlineCount}} online",
260
264
  "{{memberCount}} members, {{onlineCount}} online_many": "{{memberCount}} members, {{onlineCount}} online",
@@ -284,6 +288,7 @@
284
288
  "a11y/Message from you": "Message from you",
285
289
  "a11y/Message from {{sender}}": "Message from {{sender}}",
286
290
  "a11y/Muted": "Muted",
291
+ "a11y/Pinned": "Pinned",
287
292
  "a11y/New message from {{user}}": "New message from {{user}}",
288
293
  "a11y/Offline": "Offline",
289
294
  "a11y/Open message actions": "Open message actions",
package/src/i18n/es.json CHANGED
@@ -274,6 +274,7 @@
274
274
  "a11y/Loading failed": "Error al cargar",
275
275
  "a11y/Message actions": "Acciones del mensaje",
276
276
  "a11y/Muted": "Silenciado",
277
+ "a11y/Pinned": "Fijado",
277
278
  "a11y/New message from {{user}}": "Nuevo mensaje de {{user}}",
278
279
  "a11y/Offline": "Sin conexión",
279
280
  "a11y/Open message actions": "Abrir acciones del mensaje",
@@ -435,5 +436,9 @@
435
436
  "a11y/Message from {{sender}}": "Mensaje de {{sender}}",
436
437
  "a11y/Gallery Image": "Imagen de la galería",
437
438
  "a11y/Gallery Video": "Vídeo de la galería",
438
- "a11y/{{position}} of {{count}}": "{{position}} de {{count}}"
439
+ "a11y/{{position}} of {{count}}": "{{position}} de {{count}}",
440
+ "Pin Chat": "Fijar chat",
441
+ "Pin Group": "Fijar grupo",
442
+ "Unpin Chat": "Desfijar chat",
443
+ "Unpin Group": "Desfijar grupo"
439
444
  }