stream-chat-react-native-core 9.0.0-beta.14 → 9.0.0-beta.16

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 (108) hide show
  1. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js +1 -1
  2. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  3. package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -1
  4. package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
  5. package/lib/commonjs/components/Channel/Channel.js +62 -17
  6. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  7. package/lib/commonjs/components/ChannelList/hooks/useChannelActionItems.js +1 -1
  8. package/lib/commonjs/components/ChannelList/hooks/useChannelActionItems.js.map +1 -1
  9. package/lib/commonjs/components/ChannelPreview/ChannelDetailsBottomSheet.js +17 -2
  10. package/lib/commonjs/components/ChannelPreview/ChannelDetailsBottomSheet.js.map +1 -1
  11. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js +5 -4
  12. package/lib/commonjs/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  13. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js +26 -31
  14. package/lib/commonjs/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  15. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js +65 -36
  16. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  17. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.js +1 -1
  18. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.js.map +1 -1
  19. package/lib/commonjs/components/Message/utils/messageActions.js +1 -1
  20. package/lib/commonjs/components/Message/utils/messageActions.js.map +1 -1
  21. package/lib/commonjs/components/MessageMenu/MessageUserReactions.js +3 -1
  22. package/lib/commonjs/components/MessageMenu/MessageUserReactions.js.map +1 -1
  23. package/lib/commonjs/components/UIComponents/BottomSheetModal.js +4 -2
  24. package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
  25. package/lib/commonjs/icons/index.js +12 -0
  26. package/lib/commonjs/icons/index.js.map +1 -1
  27. package/lib/commonjs/store/OfflineDB.js +1 -0
  28. package/lib/commonjs/store/OfflineDB.js.map +1 -1
  29. package/lib/commonjs/store/apis/index.js +11 -0
  30. package/lib/commonjs/store/apis/index.js.map +1 -1
  31. package/lib/commonjs/store/apis/updatePendingTask.js +34 -0
  32. package/lib/commonjs/store/apis/updatePendingTask.js.map +1 -0
  33. package/lib/commonjs/version.json +1 -1
  34. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js +1 -1
  35. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  36. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -1
  37. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
  38. package/lib/module/components/Channel/Channel.js +62 -17
  39. package/lib/module/components/Channel/Channel.js.map +1 -1
  40. package/lib/module/components/ChannelList/hooks/useChannelActionItems.js +1 -1
  41. package/lib/module/components/ChannelList/hooks/useChannelActionItems.js.map +1 -1
  42. package/lib/module/components/ChannelPreview/ChannelDetailsBottomSheet.js +17 -2
  43. package/lib/module/components/ChannelPreview/ChannelDetailsBottomSheet.js.map +1 -1
  44. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js +5 -4
  45. package/lib/module/components/ChannelPreview/ChannelPreviewMessage.js.map +1 -1
  46. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js +26 -31
  47. package/lib/module/components/ChannelPreview/ChannelSwipableWrapper.js.map +1 -1
  48. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js +65 -36
  49. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  50. package/lib/module/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.js +1 -1
  51. package/lib/module/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.js.map +1 -1
  52. package/lib/module/components/Message/utils/messageActions.js +1 -1
  53. package/lib/module/components/Message/utils/messageActions.js.map +1 -1
  54. package/lib/module/components/MessageMenu/MessageUserReactions.js +3 -1
  55. package/lib/module/components/MessageMenu/MessageUserReactions.js.map +1 -1
  56. package/lib/module/components/UIComponents/BottomSheetModal.js +4 -2
  57. package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
  58. package/lib/module/icons/index.js +12 -0
  59. package/lib/module/icons/index.js.map +1 -1
  60. package/lib/module/store/OfflineDB.js +1 -0
  61. package/lib/module/store/OfflineDB.js.map +1 -1
  62. package/lib/module/store/apis/index.js +11 -0
  63. package/lib/module/store/apis/index.js.map +1 -1
  64. package/lib/module/store/apis/updatePendingTask.js +34 -0
  65. package/lib/module/store/apis/updatePendingTask.js.map +1 -0
  66. package/lib/module/version.json +1 -1
  67. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  68. package/lib/typescript/components/ChannelPreview/ChannelDetailsBottomSheet.d.ts.map +1 -1
  69. package/lib/typescript/components/ChannelPreview/ChannelPreviewMessage.d.ts.map +1 -1
  70. package/lib/typescript/components/ChannelPreview/ChannelSwipableWrapper.d.ts.map +1 -1
  71. package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts +3 -2
  72. package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts.map +1 -1
  73. package/lib/typescript/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.d.ts.map +1 -1
  74. package/lib/typescript/components/MessageMenu/MessageUserReactions.d.ts.map +1 -1
  75. package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
  76. package/lib/typescript/icons/index.d.ts +1 -0
  77. package/lib/typescript/icons/index.d.ts.map +1 -1
  78. package/lib/typescript/store/OfflineDB.d.ts +1 -0
  79. package/lib/typescript/store/OfflineDB.d.ts.map +1 -1
  80. package/lib/typescript/store/apis/index.d.ts +1 -0
  81. package/lib/typescript/store/apis/index.d.ts.map +1 -1
  82. package/lib/typescript/store/apis/updatePendingTask.d.ts +3 -0
  83. package/lib/typescript/store/apis/updatePendingTask.d.ts.map +1 -0
  84. package/lib/typescript/store/mappers/mapTaskToStorable.d.ts +9 -0
  85. package/lib/typescript/store/mappers/mapTaskToStorable.d.ts.map +1 -1
  86. package/package.json +2 -2
  87. package/src/__tests__/offline-support/optimistic-update.js +225 -0
  88. package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx +2 -2
  89. package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx +2 -2
  90. package/src/components/Channel/Channel.tsx +50 -4
  91. package/src/components/ChannelList/hooks/__tests__/useChannelActionItems.test.tsx +2 -2
  92. package/src/components/ChannelList/hooks/useChannelActionItems.tsx +1 -1
  93. package/src/components/ChannelPreview/ChannelDetailsBottomSheet.tsx +15 -1
  94. package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +7 -2
  95. package/src/components/ChannelPreview/ChannelSwipableWrapper.tsx +26 -29
  96. package/src/components/ChannelPreview/__tests__/ChannelSwipableWrapper.test.tsx +201 -0
  97. package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx +43 -25
  98. package/src/components/KeyboardCompatibleView/KeyboardControllerAvoidingView.tsx +1 -7
  99. package/src/components/Message/utils/messageActions.ts +1 -1
  100. package/src/components/MessageMenu/MessageUserReactions.tsx +3 -1
  101. package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx +9 -2
  102. package/src/components/UIComponents/BottomSheetModal.tsx +4 -2
  103. package/src/icons/index.ts +1 -0
  104. package/src/store/OfflineDB.ts +2 -0
  105. package/src/store/apis/__tests__/updatePendingTask.test.ts +66 -0
  106. package/src/store/apis/index.ts +1 -0
  107. package/src/store/apis/updatePendingTask.ts +24 -0
  108. package/src/version.json +1 -1
@@ -0,0 +1,201 @@
1
+ import React from 'react';
2
+ import { Text } from 'react-native';
3
+
4
+ import { act, render } from '@testing-library/react-native';
5
+ import type { Channel } from 'stream-chat';
6
+
7
+ import type { ChannelActionItem } from '../../ChannelList/hooks/useChannelActionItems';
8
+ import * as ChannelActionItemsModule from '../../ChannelList/hooks/useChannelActionItems';
9
+ import * as ChannelActionsModule from '../../ChannelList/hooks/useChannelActions';
10
+ import { ChannelSwipableWrapper } from '../ChannelSwipableWrapper';
11
+ import * as UseIsChannelMutedModule from '../hooks/useIsChannelMuted';
12
+
13
+ const rightActionsProbe = {
14
+ items: [] as Array<{ action: () => void; id: string }>,
15
+ };
16
+
17
+ const mockSwipableWrapper = jest.fn(
18
+ ({
19
+ children,
20
+ swipableProps,
21
+ }: React.PropsWithChildren<{
22
+ swipableProps?: { renderRightActions?: (...args: never[]) => void };
23
+ }>) => {
24
+ const rightActions = swipableProps?.renderRightActions?.({} as never, {} as never);
25
+ return (
26
+ <>
27
+ {children}
28
+ {rightActions}
29
+ </>
30
+ );
31
+ },
32
+ );
33
+
34
+ jest.mock('../../../contexts', () => ({
35
+ useTheme: () => ({
36
+ theme: {
37
+ semantics: {
38
+ accentPrimary: '#00f',
39
+ backgroundCoreSurface: '#fff',
40
+ textOnAccent: '#000',
41
+ textPrimary: '#111',
42
+ },
43
+ },
44
+ }),
45
+ }));
46
+
47
+ jest.mock('../../../contexts/swipeableContext/SwipeRegistryContext', () => ({
48
+ useSwipeRegistryContext: () => ({
49
+ closeAll: jest.fn(),
50
+ }),
51
+ }));
52
+
53
+ jest.mock('../../UIComponents', () => ({
54
+ BottomSheetModal: ({ children }: React.PropsWithChildren) => <>{children}</>,
55
+ RightActions: ({ items }: { items: Array<{ action: () => void; id: string }> }) => {
56
+ rightActionsProbe.items = items;
57
+ return null;
58
+ },
59
+ SwipableWrapper: (...args: unknown[]) => mockSwipableWrapper(...args),
60
+ }));
61
+
62
+ describe('ChannelSwipableWrapper', () => {
63
+ const channel = { cid: 'messaging:test-channel', id: 'test-channel' } as Channel;
64
+
65
+ beforeEach(() => {
66
+ jest.clearAllMocks();
67
+ rightActionsProbe.items = [];
68
+ });
69
+
70
+ it('uses channel mute for direct-channel swipe actions and keeps mute user in the sheet', () => {
71
+ const muteChannel = jest.fn();
72
+ const unmuteChannel = jest.fn();
73
+ const customBottomSheet = jest.fn(() => null);
74
+ const items: ChannelActionItem[] = [
75
+ {
76
+ Icon: () => null,
77
+ action: jest.fn(),
78
+ id: 'mute',
79
+ label: 'Mute User',
80
+ placement: 'sheet',
81
+ type: 'standard',
82
+ },
83
+ {
84
+ Icon: () => null,
85
+ action: jest.fn(),
86
+ id: 'archive',
87
+ label: 'Archive Chat',
88
+ placement: 'sheet',
89
+ type: 'standard',
90
+ },
91
+ ];
92
+
93
+ jest.spyOn(ChannelActionsModule, 'useChannelActions').mockReturnValue({
94
+ archive: jest.fn(),
95
+ blockUser: jest.fn(),
96
+ deleteChannel: jest.fn(),
97
+ leave: jest.fn(),
98
+ muteChannel,
99
+ muteUser: jest.fn(),
100
+ pin: jest.fn(),
101
+ unarchive: jest.fn(),
102
+ unblockUser: jest.fn(),
103
+ unmuteChannel,
104
+ unmuteUser: jest.fn(),
105
+ unpin: jest.fn(),
106
+ });
107
+ jest.spyOn(ChannelActionItemsModule, 'useChannelActionItems').mockReturnValue(items);
108
+ jest.spyOn(UseIsChannelMutedModule, 'useIsChannelMuted').mockReturnValue({
109
+ createdAt: null,
110
+ expiresAt: null,
111
+ muted: false,
112
+ });
113
+
114
+ render(
115
+ <ChannelSwipableWrapper channel={channel} ChannelDetailsBottomSheet={customBottomSheet}>
116
+ <Text>child</Text>
117
+ </ChannelSwipableWrapper>,
118
+ );
119
+
120
+ expect(customBottomSheet).toHaveBeenCalledWith(
121
+ expect.objectContaining({
122
+ channel,
123
+ items,
124
+ }),
125
+ undefined,
126
+ );
127
+ expect(rightActionsProbe.items.map((item) => item.id)).toEqual(['openSheet', 'mute']);
128
+
129
+ act(() => {
130
+ rightActionsProbe.items[1].action();
131
+ });
132
+
133
+ expect(muteChannel).toHaveBeenCalledTimes(1);
134
+ expect(unmuteChannel).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('removes mute group from the sheet while keeping mute as the quick swipe action', () => {
138
+ const muteChannel = jest.fn();
139
+ const customBottomSheet = jest.fn(() => null);
140
+ const muteItem = {
141
+ Icon: () => null,
142
+ action: jest.fn(),
143
+ id: 'mute',
144
+ label: 'Mute Group',
145
+ placement: 'swipe',
146
+ type: 'standard',
147
+ } as const satisfies ChannelActionItem;
148
+ const archiveItem = {
149
+ Icon: () => null,
150
+ action: jest.fn(),
151
+ id: 'archive',
152
+ label: 'Archive Group',
153
+ placement: 'both',
154
+ type: 'standard',
155
+ } as const satisfies ChannelActionItem;
156
+
157
+ jest.spyOn(ChannelActionsModule, 'useChannelActions').mockReturnValue({
158
+ archive: jest.fn(),
159
+ blockUser: jest.fn(),
160
+ deleteChannel: jest.fn(),
161
+ leave: jest.fn(),
162
+ muteChannel,
163
+ muteUser: jest.fn(),
164
+ pin: jest.fn(),
165
+ unarchive: jest.fn(),
166
+ unblockUser: jest.fn(),
167
+ unmuteChannel: jest.fn(),
168
+ unmuteUser: jest.fn(),
169
+ unpin: jest.fn(),
170
+ });
171
+ jest
172
+ .spyOn(ChannelActionItemsModule, 'useChannelActionItems')
173
+ .mockReturnValue([muteItem, archiveItem]);
174
+ jest.spyOn(UseIsChannelMutedModule, 'useIsChannelMuted').mockReturnValue({
175
+ createdAt: null,
176
+ expiresAt: null,
177
+ muted: false,
178
+ });
179
+
180
+ render(
181
+ <ChannelSwipableWrapper channel={channel} ChannelDetailsBottomSheet={customBottomSheet}>
182
+ <Text>child</Text>
183
+ </ChannelSwipableWrapper>,
184
+ );
185
+
186
+ expect(customBottomSheet).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ channel,
189
+ items: [archiveItem],
190
+ }),
191
+ undefined,
192
+ );
193
+ expect(rightActionsProbe.items.map((item) => item.id)).toEqual(['openSheet', 'mute']);
194
+
195
+ act(() => {
196
+ rightActionsProbe.items[1].action();
197
+ });
198
+
199
+ expect(muteChannel).toHaveBeenCalledTimes(1);
200
+ });
201
+ });
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  AppState,
4
5
  AppStateStatus,
5
6
  Dimensions,
@@ -39,7 +40,7 @@ export class KeyboardCompatibleView extends React.Component<
39
40
  KeyboardAvoidingViewProps,
40
41
  'behavior' | 'enabled' | 'keyboardVerticalOffset'
41
42
  > = {
42
- behavior: Platform.OS === 'ios' ? 'padding' : 'position',
43
+ behavior: 'padding',
43
44
  enabled: true,
44
45
  keyboardVerticalOffset: Platform.OS === 'ios' ? 86.5 : -300, // default MessageComposer height
45
46
  };
@@ -62,35 +63,40 @@ export class KeyboardCompatibleView extends React.Component<
62
63
  this.viewRef = React.createRef();
63
64
  }
64
65
 
65
- _relativeKeyboardHeight(keyboardFrame: KeyboardMetrics) {
66
+ async _relativeKeyboardHeight(keyboardFrame: KeyboardMetrics): Promise<number> {
66
67
  const frame = this._frame;
67
- /**
68
- * With iOS 14 & Reduce Motion > Prefer Cross-Fade Transitions enabled, the keyboard position
69
- * height is reported differently (0 instead of Y position value) which caused the view to take full height
70
- * of the screen.
71
- */
72
- if (!frame || !keyboardFrame || keyboardFrame.screenY === 0) {
68
+ if (!frame || !keyboardFrame) {
69
+ return 0;
70
+ }
71
+
72
+ if (
73
+ Platform.OS === 'ios' &&
74
+ keyboardFrame.screenY === 0 &&
75
+ (await AccessibilityInfo.prefersCrossFadeTransitions())
76
+ ) {
73
77
  return 0;
74
78
  }
75
79
 
76
80
  const keyboardY = keyboardFrame.screenY - (this.props.keyboardVerticalOffset ?? 0);
77
- const relativeHeight = frame.y + frame.height - keyboardY;
78
81
 
79
- /**
80
- * When the StatusBar is translucent there is an issue
81
- * where the relative keyboard height is returned incorrectly
82
- * instead of 0 when closed.
83
- */
82
+ if (this.props.behavior === 'height') {
83
+ return Math.max(this.state.bottom + frame.y + frame.height - keyboardY, 0);
84
+ }
85
+
86
+ // Calculate the displacement needed for the view such that it
87
+ // no longer overlaps with the keyboard
88
+ const relativeHeight = Math.max(frame.y + frame.height - keyboardY, 0);
89
+
84
90
  if (Platform.OS === 'android') {
85
91
  const barHeights = Dimensions.get('screen').height - Dimensions.get('window').height;
86
- if (relativeHeight <= Math.max(barHeights, StatusBar.currentHeight ?? 0)) {
92
+ const systemInsetFloor = Math.max(barHeights, StatusBar.currentHeight ?? 0);
93
+
94
+ if (relativeHeight <= systemInsetFloor) {
87
95
  return 0;
88
96
  }
89
97
  }
90
98
 
91
- // Calculate the displacement needed for the view such that it
92
- // no longer overlaps with the keyboard
93
- return Math.max(relativeHeight, 0);
99
+ return relativeHeight;
94
100
  }
95
101
 
96
102
  _onKeyboardChange: KeyboardEventListener = (event) => {
@@ -98,7 +104,12 @@ export class KeyboardCompatibleView extends React.Component<
98
104
  this._updateBottomIfNecessary();
99
105
  };
100
106
 
101
- _onLayout: (event: LayoutChangeEvent) => void = (event) => {
107
+ _onKeyboardHide: KeyboardEventListener = () => {
108
+ this._keyboardEvent = null;
109
+ this._updateBottomIfNecessary();
110
+ };
111
+
112
+ _onLayout: (event: LayoutChangeEvent) => void = async (event) => {
102
113
  event.persist();
103
114
 
104
115
  const oldFrame = this._frame;
@@ -110,7 +121,7 @@ export class KeyboardCompatibleView extends React.Component<
110
121
 
111
122
  // update bottom height for the first time or when the height is changed
112
123
  if (!oldFrame || oldFrame.height !== this._frame.height) {
113
- this._updateBottomIfNecessary();
124
+ await this._updateBottomIfNecessary();
114
125
  }
115
126
 
116
127
  if (this.props.onLayout) {
@@ -127,14 +138,14 @@ export class KeyboardCompatibleView extends React.Component<
127
138
  }
128
139
  };
129
140
 
130
- _updateBottomIfNecessary = () => {
141
+ _updateBottomIfNecessary = async () => {
131
142
  if (this._keyboardEvent == null) {
132
143
  this._setBottom(0);
133
144
  return;
134
145
  }
135
146
 
136
147
  const { duration, easing, endCoordinates } = this._keyboardEvent;
137
- const height = this._relativeKeyboardHeight(endCoordinates);
148
+ const height = await this._relativeKeyboardHeight(endCoordinates);
138
149
 
139
150
  if (this._bottom === height) {
140
151
  return;
@@ -142,7 +153,8 @@ export class KeyboardCompatibleView extends React.Component<
142
153
 
143
154
  this._setBottom(height);
144
155
 
145
- if (duration && easing) {
156
+ const enabled = this.props.enabled ?? true;
157
+ if (enabled && duration && easing) {
146
158
  LayoutAnimation.configureNext({
147
159
  // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m
148
160
  duration: duration > 10 ? duration : 10,
@@ -169,11 +181,12 @@ export class KeyboardCompatibleView extends React.Component<
169
181
  setKeyboardListeners = () => {
170
182
  if (Platform.OS === 'ios') {
171
183
  this._subscriptions = [
172
- Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange),
184
+ Keyboard.addListener('keyboardWillHide', this._onKeyboardHide),
185
+ Keyboard.addListener('keyboardWillShow', this._onKeyboardChange),
173
186
  ];
174
187
  } else {
175
188
  this._subscriptions = [
176
- Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
189
+ Keyboard.addListener('keyboardDidHide', this._onKeyboardHide),
177
190
  Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
178
191
  ];
179
192
  }
@@ -218,6 +231,11 @@ export class KeyboardCompatibleView extends React.Component<
218
231
  }
219
232
 
220
233
  componentDidMount() {
234
+ if (!Keyboard.isVisible()) {
235
+ this._keyboardEvent = null;
236
+ this._setBottom(0);
237
+ }
238
+
221
239
  this._appStateSubscription = AppState.addEventListener('change', this._handleAppStateChange);
222
240
  this.setKeyboardListeners();
223
241
  }
@@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
2
2
 
3
3
  import {
4
4
  Keyboard,
5
- Platform,
6
5
  KeyboardAvoidingViewProps as ReactNativeKeyboardAvoidingViewProps,
7
6
  } from 'react-native';
8
7
 
@@ -51,12 +50,7 @@ export const KeyboardCompatibleView = (props: KeyboardCompatibleViewProps) => {
51
50
  </KeyboardProvider>
52
51
  );
53
52
  }
54
- const compatibleBehavior =
55
- behavior === 'translate-with-padding'
56
- ? Platform.OS === 'ios'
57
- ? 'padding'
58
- : 'position'
59
- : behavior;
53
+ const compatibleBehavior = behavior === 'translate-with-padding' ? 'padding' : behavior;
60
54
 
61
55
  return (
62
56
  <KeyboardCompatibleViewDefault behavior={compatibleBehavior} {...rest}>
@@ -90,7 +90,7 @@ export const messageActions = ({
90
90
  actions.push(copyMessage);
91
91
  }
92
92
 
93
- if (ownCapabilities.readEvents && !error && !isThreadMessage) {
93
+ if (ownCapabilities.readEvents && !error && !isThreadMessage && !isMyMessage) {
94
94
  actions.push(markUnread);
95
95
  }
96
96
 
@@ -125,7 +125,9 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => {
125
125
  const MessageUserReactionsItem = propMessageUserReactionsItem ?? contextMessageUserReactionsItem;
126
126
 
127
127
  const onSelectReaction = useStableCallback((reactionType: string) => {
128
- setSelectedReaction(reactionType);
128
+ setSelectedReaction((currentReaction) =>
129
+ currentReaction === reactionType ? undefined : reactionType,
130
+ );
129
131
  });
130
132
 
131
133
  useEffect(() => {
@@ -100,14 +100,21 @@ describe('MessageUserReactions when the supportedReactions are defined', () => {
100
100
  expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-unselected');
101
101
  });
102
102
 
103
- it('changes selected reaction when a reaction button is pressed', () => {
103
+ it('toggles the selected reaction when a reaction button is pressed twice', () => {
104
104
  const { getAllByLabelText } = renderComponent();
105
- const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/);
105
+ let reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/);
106
106
 
107
107
  fireEvent.press(reactionButtons[1]);
108
108
 
109
109
  expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-unselected');
110
110
  expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-selected');
111
+
112
+ fireEvent.press(reactionButtons[1]);
113
+
114
+ reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/);
115
+
116
+ expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-unselected');
117
+ expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-unselected');
111
118
  });
112
119
 
113
120
  it('renders reactions list', () => {
@@ -242,8 +242,10 @@ export const BottomSheetModal = (props: PropsWithChildren<BottomSheetModalProps>
242
242
  KeyboardControllerPackage.KeyboardEvents.addListener('keyboardDidHide', keyboardDidHide),
243
243
  );
244
244
  } else {
245
- listeners.push(Keyboard.addListener('keyboardDidShow', keyboardDidShowRN));
246
- listeners.push(Keyboard.addListener('keyboardDidHide', keyboardDidHide));
245
+ if (Platform.OS === 'ios') {
246
+ listeners.push(Keyboard.addListener('keyboardWillShow', keyboardDidShowRN));
247
+ listeners.push(Keyboard.addListener('keyboardWillHide', keyboardDidHide));
248
+ }
247
249
  }
248
250
 
249
251
  return () => listeners.forEach((l) => l.remove());
@@ -34,6 +34,7 @@ export * from './clock';
34
34
  export * from './Unknown';
35
35
  export * from './unpin';
36
36
  export * from './video-fill';
37
+ export * from './video';
37
38
  export * from './user-add';
38
39
  export * from './user-remove';
39
40
  export * from './filetype-video-xl';
@@ -67,6 +67,8 @@ export class OfflineDB extends AbstractOfflineDB {
67
67
 
68
68
  addPendingTask = api.addPendingTask;
69
69
 
70
+ updatePendingTask = api.updatePendingTask;
71
+
70
72
  deletePendingTask = api.deletePendingTask;
71
73
 
72
74
  deleteReaction = api.deleteReaction;
@@ -0,0 +1,66 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ import { addPendingTask, getPendingTasks, updatePendingTask } from '..';
4
+ import { generateMessage } from '../../../mock-builders/generator/message';
5
+ import { BetterSqlite } from '../../../test-utils/BetterSqlite';
6
+ import { SqliteClient } from '../../SqliteClient';
7
+
8
+ describe('updatePendingTask', () => {
9
+ beforeEach(async () => {
10
+ await SqliteClient.initializeDatabase();
11
+ await BetterSqlite.openDB();
12
+ });
13
+
14
+ afterEach(() => {
15
+ BetterSqlite.dropAllTables();
16
+ BetterSqlite.closeDB();
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it('should replace an existing pending task row by id without changing its createdAt ordering', async () => {
21
+ const channelId = uuidv4();
22
+ const originalMessage = generateMessage({
23
+ cid: `messaging:${channelId}`,
24
+ id: uuidv4(),
25
+ text: 'original text',
26
+ });
27
+
28
+ await addPendingTask({
29
+ channelId,
30
+ channelType: 'messaging',
31
+ messageId: originalMessage.id,
32
+ payload: [originalMessage, {}],
33
+ type: 'send-message',
34
+ });
35
+
36
+ const [originalRow] = await BetterSqlite.selectFromTable('pendingTasks');
37
+ const [originalTask] = await getPendingTasks({ messageId: originalMessage.id });
38
+
39
+ const editedMessage = {
40
+ ...originalMessage,
41
+ text: 'edited text',
42
+ };
43
+
44
+ await updatePendingTask({
45
+ id: originalTask.id,
46
+ task: {
47
+ channelId,
48
+ channelType: 'messaging',
49
+ messageId: originalMessage.id,
50
+ payload: [editedMessage, {}],
51
+ type: 'send-message',
52
+ },
53
+ });
54
+
55
+ const [updatedRow] = await BetterSqlite.selectFromTable('pendingTasks');
56
+ const [updatedTask] = await getPendingTasks({ messageId: originalMessage.id });
57
+
58
+ expect(updatedRow.id).toBe(originalRow.id);
59
+ expect(updatedRow.createdAt).toBe(originalRow.createdAt);
60
+ expect(updatedRow.type).toBe('send-message');
61
+ expect(JSON.parse(updatedRow.payload)[0].text).toBe('edited text');
62
+ expect(updatedTask.id).toBe(originalTask.id);
63
+ expect(updatedTask.type).toBe('send-message');
64
+ expect(updatedTask.payload[0].text).toBe('edited text');
65
+ });
66
+ });
@@ -13,6 +13,7 @@ export * from './getLastSyncedAt';
13
13
  export * from './getMembers';
14
14
  export * from './getReads';
15
15
  export * from './updateMessage';
16
+ export * from './updatePendingTask';
16
17
  export * from './updateReaction';
17
18
  export * from './insertReaction';
18
19
  export * from './deleteReaction';
@@ -0,0 +1,24 @@
1
+ import type { DBUpdatePendingTaskType } from 'stream-chat';
2
+
3
+ import { mapTaskToStorable } from '../mappers/mapTaskToStorable';
4
+ import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
5
+ import { SqliteClient } from '../SqliteClient';
6
+
7
+ export const updatePendingTask = async ({ id, task }: DBUpdatePendingTaskType) => {
8
+ const storableTask = mapTaskToStorable(task);
9
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
+ const { createdAt, id: taskId, ...nextTask } = storableTask;
11
+
12
+ const query = createUpdateQuery('pendingTasks', nextTask, {
13
+ id,
14
+ });
15
+
16
+ SqliteClient.logger?.('info', 'updatePendingTask', {
17
+ id,
18
+ task: nextTask,
19
+ });
20
+
21
+ await SqliteClient.executeSql.apply(null, query);
22
+
23
+ return [query];
24
+ };
package/src/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "9.0.0-beta.14"
2
+ "version": "9.0.0-beta.16"
3
3
  }