diff --git a/README.md b/README.md
index 8a31481367..a93c62eca5 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
[](https://www.npmjs.com/package/stream-chat-react-native)
[](https://github.com/GetStream/stream-chat-react-native/actions)
[](https://getstream.io/chat/docs/sdk/reactnative)
-
+
diff --git a/examples/ExpoMessaging/app/index.tsx b/examples/ExpoMessaging/app/index.tsx
index e484cccbfa..bfefd0dee0 100644
--- a/examples/ExpoMessaging/app/index.tsx
+++ b/examples/ExpoMessaging/app/index.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { Alert, Image, Pressable, StyleSheet, View } from 'react-native';
import { ChannelList, SqliteClient } from 'stream-chat-expo';
import { useCallback, useContext, useMemo } from 'react';
diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock
index b0654bc72e..104da149ca 100644
--- a/examples/ExpoMessaging/yarn.lock
+++ b/examples/ExpoMessaging/yarn.lock
@@ -4272,6 +4272,22 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1"
semver "^7.5.4"
+jsonwebtoken@^9.0.3:
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
+ integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==
+ dependencies:
+ jws "^4.0.1"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^7.5.4"
+
jwa@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.2.tgz#16011ac6db48de7b102777e57897901520eec7b9"
@@ -4281,6 +4297,15 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
+jwa@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
+ integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==
+ dependencies:
+ buffer-equal-constant-time "^1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@@ -4289,6 +4314,14 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
+jws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
+ integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==
+ dependencies:
+ jwa "^2.0.1"
+ safe-buffer "^5.0.1"
+
kleur@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
@@ -6019,10 +6052,10 @@ stream-chat-react-native-core@8.1.0:
version "0.0.0"
uid ""
-stream-chat@^9.23.0:
- version "9.23.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.23.0.tgz#e7e5cf729861597e7198907c1cab22a57d68a2fc"
- integrity sha512-UW112HYsLnYb4RMIXBtAouNQCCe0weVzNivjezsw+JKK1b/TX0JLBi+wK25mBUEO+coOGKfXiye6IB3gao8ipw==
+stream-chat@^9.27.2:
+ version "9.27.2"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
+ integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -6030,7 +6063,7 @@ stream-chat@^9.23.0:
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
- jsonwebtoken "^9.0.2"
+ jsonwebtoken "^9.0.3"
linkifyjs "^4.3.2"
ws "^8.18.1"
diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock
index 3737289497..5d203517a1 100644
--- a/examples/SampleApp/yarn.lock
+++ b/examples/SampleApp/yarn.lock
@@ -3819,7 +3819,7 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
-buffer-equal-constant-time@1.0.1:
+buffer-equal-constant-time@1.0.1, buffer-equal-constant-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
@@ -6476,6 +6476,22 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1"
semver "^7.5.4"
+jsonwebtoken@^9.0.3:
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
+ integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==
+ dependencies:
+ jws "^4.0.1"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^7.5.4"
+
"jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.3.5"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
@@ -6495,6 +6511,15 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
+jwa@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
+ integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==
+ dependencies:
+ buffer-equal-constant-time "^1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@@ -6503,6 +6528,14 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
+jws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
+ integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==
+ dependencies:
+ jwa "^2.0.1"
+ safe-buffer "^5.0.1"
+
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -8280,34 +8313,17 @@ stream-chat-react-native-core@8.1.0:
use-sync-external-store "^1.5.0"
"stream-chat-react-native-core@link:../../package":
- version "8.1.0"
- dependencies:
- "@gorhom/bottom-sheet" "^5.1.8"
- "@ungap/structured-clone" "^1.3.0"
- dayjs "1.11.13"
- emoji-regex "^10.4.0"
- i18next "^25.2.1"
- intl-pluralrules "^2.0.1"
- linkifyjs "^4.3.1"
- lodash-es "4.17.21"
- mime-types "^2.1.35"
- path "0.12.7"
- react-native-markdown-package "1.8.2"
- react-native-url-polyfill "^2.0.0"
- stream-chat "^9.23.0"
- use-sync-external-store "^1.5.0"
+ version "0.0.0"
+ uid ""
"stream-chat-react-native@link:../../package/native-package":
- version "8.1.0"
- dependencies:
- es6-symbol "^3.1.3"
- mime "^4.0.7"
- stream-chat-react-native-core "8.1.0"
+ version "0.0.0"
+ uid ""
-stream-chat@^9.23.0:
- version "9.23.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.23.0.tgz#e7e5cf729861597e7198907c1cab22a57d68a2fc"
- integrity sha512-UW112HYsLnYb4RMIXBtAouNQCCe0weVzNivjezsw+JKK1b/TX0JLBi+wK25mBUEO+coOGKfXiye6IB3gao8ipw==
+stream-chat@^9.27.2:
+ version "9.27.2"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
+ integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -8315,7 +8331,7 @@ stream-chat@^9.23.0:
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
- jsonwebtoken "^9.0.2"
+ jsonwebtoken "^9.0.3"
linkifyjs "^4.3.2"
ws "^8.18.1"
diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock
index 403761eb4d..747c18b03c 100644
--- a/examples/TypeScriptMessaging/yarn.lock
+++ b/examples/TypeScriptMessaging/yarn.lock
@@ -3045,7 +3045,7 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
-buffer-equal-constant-time@1.0.1:
+buffer-equal-constant-time@1.0.1, buffer-equal-constant-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
@@ -5646,6 +5646,22 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1"
semver "^7.5.4"
+jsonwebtoken@^9.0.3:
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2"
+ integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==
+ dependencies:
+ jws "^4.0.1"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^7.5.4"
+
"jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.3.5"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
@@ -5665,6 +5681,15 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
+jwa@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804"
+ integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==
+ dependencies:
+ buffer-equal-constant-time "^1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@@ -5673,6 +5698,14 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
+jws@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690"
+ integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==
+ dependencies:
+ jwa "^2.0.1"
+ safe-buffer "^5.0.1"
+
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -7381,34 +7414,17 @@ stream-chat-react-native-core@8.1.0:
use-sync-external-store "^1.5.0"
"stream-chat-react-native-core@link:../../package":
- version "8.1.0"
- dependencies:
- "@gorhom/bottom-sheet" "^5.1.8"
- "@ungap/structured-clone" "^1.3.0"
- dayjs "1.11.13"
- emoji-regex "^10.4.0"
- i18next "^25.2.1"
- intl-pluralrules "^2.0.1"
- linkifyjs "^4.3.1"
- lodash-es "4.17.21"
- mime-types "^2.1.35"
- path "0.12.7"
- react-native-markdown-package "1.8.2"
- react-native-url-polyfill "^2.0.0"
- stream-chat "^9.23.0"
- use-sync-external-store "^1.5.0"
+ version "0.0.0"
+ uid ""
"stream-chat-react-native@link:../../package/native-package":
- version "8.1.0"
- dependencies:
- es6-symbol "^3.1.3"
- mime "^4.0.7"
- stream-chat-react-native-core "8.1.0"
+ version "0.0.0"
+ uid ""
-stream-chat@^9.23.0:
- version "9.24.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.24.0.tgz#e6af5d4b0eb396e24e0ab7f852719581c39f18bc"
- integrity sha512-zLtguYRqxeEc/Cjw8Zp00u/wTrqFg4gFPKdj3mvl/Jq1Pt95mY9nMc38KW0GOu/2quIAAar0NNMq8fsXl4jupQ==
+stream-chat@^9.27.2:
+ version "9.27.2"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968"
+ integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -7416,7 +7432,7 @@ stream-chat@^9.23.0:
base64-js "^1.5.1"
form-data "^4.0.4"
isomorphic-ws "^5.0.0"
- jsonwebtoken "^9.0.2"
+ jsonwebtoken "^9.0.3"
linkifyjs "^4.3.2"
ws "^8.18.1"
diff --git a/package/package.json b/package/package.json
index 6766b0bc53..0f4028fa85 100644
--- a/package/package.json
+++ b/package/package.json
@@ -80,7 +80,7 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
- "stream-chat": "^9.26.0",
+ "stream-chat": "^9.27.2",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 5685271ac9..723cd9b907 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -106,7 +106,11 @@ import {
isImagePickerAvailable,
NativeHandlers,
} from '../../native';
-import { ChannelUnreadState, FileTypes } from '../../types/types';
+import {
+ ChannelUnreadStateStore,
+ ChannelUnreadStateStoreType,
+} from '../../state-store/channel-unread-state';
+import { FileTypes } from '../../types/types';
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
import { compressedImageURI } from '../../utils/compressImage';
import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand';
@@ -149,6 +153,7 @@ import { LoadingIndicator as LoadingIndicatorDefault } from '../Indicators/Loadi
import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from '../KeyboardCompatibleView/KeyboardCompatibleView';
import { Message as MessageDefault } from '../Message/Message';
import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar';
+import { MessageBlocked as MessageBlockedDefault } from '../Message/MessageSimple/MessageBlocked';
import { MessageBounce as MessageBounceDefault } from '../Message/MessageSimple/MessageBounce';
import { MessageContent as MessageContentDefault } from '../Message/MessageSimple/MessageContent';
import { MessageDeleted as MessageDeletedDefault } from '../Message/MessageSimple/MessageDeleted';
@@ -325,6 +330,7 @@ export type ChannelPropsWithContext = Pick &
| 'forceAlignMessages'
| 'Gallery'
| 'getMessagesGroupStyles'
+ | 'getMessageGroupStyle'
| 'Giphy'
| 'giphyVersion'
| 'handleBan'
@@ -354,6 +360,7 @@ export type ChannelPropsWithContext = Pick &
| 'messageActions'
| 'MessageAvatar'
| 'MessageBounce'
+ | 'MessageBlocked'
| 'MessageContent'
| 'messageContentOrder'
| 'MessageDeleted'
@@ -424,7 +431,7 @@ export type ChannelPropsWithContext = Pick &
*/
doMarkReadRequest?: (
channel: ChannelType,
- setChannelUnreadUiState?: (state: ChannelUnreadState) => void,
+ setChannelUnreadUiState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void,
) => void;
/**
* Overrides the Stream default send message request (Advanced usage only)
@@ -620,6 +627,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
forceAlignMessages,
Gallery = GalleryDefault,
getMessagesGroupStyles,
+ getMessageGroupStyle,
Giphy = GiphyDefault,
giphyVersion = 'fixed_height',
handleAttachButtonPress,
@@ -674,6 +682,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageActionListItem = MessageActionListItemDefault,
messageActions,
MessageAvatar = MessageAvatarDefault,
+ MessageBlocked = MessageBlockedDefault,
MessageBounce = MessageBounceDefault,
MessageContent = MessageContentDefault,
messageContentOrder = [
@@ -775,10 +784,13 @@ const ChannelWithContext = (props: PropsWithChildren) =
const [thread, setThread] = useState(threadProps || null);
const [threadHasMore, setThreadHasMore] = useState(true);
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
- const [channelUnreadState, setChannelUnreadState] = useState(
- undefined,
+ const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
+ const setChannelUnreadState = useCallback(
+ (data: ChannelUnreadStateStoreType['channelUnreadState']) => {
+ channelUnreadStateStore.channelUnreadState = data;
+ },
+ [channelUnreadStateStore],
);
-
const { bottomSheetRef, closePicker, openPicker } = useAttachmentPickerBottomSheet();
const syncingChannelRef = useRef(false);
@@ -903,16 +915,14 @@ const ChannelWithContext = (props: PropsWithChildren) =
}
if (event.type === 'notification.mark_unread') {
- setChannelUnreadState((prev) => {
- if (!(event.last_read_at && event.user)) {
- return prev;
- }
- return {
- first_unread_message_id: event.first_unread_message_id,
- last_read: new Date(event.last_read_at),
- last_read_message_id: event.last_read_message_id,
- unread_messages: event.unread_messages ?? 0,
- };
+ if (!(event.last_read_at && event.user)) {
+ return;
+ }
+ setChannelUnreadState({
+ first_unread_message_id: event.first_unread_message_id,
+ last_read: new Date(event.last_read_at),
+ last_read_message_id: event.last_read_message_id,
+ unread_messages: event.unread_messages ?? 0,
});
}
@@ -1433,6 +1443,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
...message,
attachments,
text: patchMessageTextCommand(text ?? '', mentioned_users ?? []),
+ // We cannot send an error message, so we convert it to a regular message.
+ type: message.type === 'error' ? 'regular' : message.type,
} as StreamMessage;
let messageResponse = {} as SendMessageAPIResponse;
@@ -1768,7 +1780,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
const channelContext = useCreateChannelContext({
channel,
- channelUnreadState,
+ channelUnreadState: channelUnreadStateStore.channelUnreadState,
+ channelUnreadStateStore,
disabled: !!channel?.data?.frozen,
EmptyStateIndicator,
enableMessageGroupingByUser,
@@ -1916,6 +1929,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
FlatList,
forceAlignMessages,
Gallery,
+ getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
@@ -1949,6 +1963,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageActionListItem,
messageActions,
MessageAvatar,
+ MessageBlocked,
MessageBounce,
MessageContent,
messageContentOrder,
diff --git a/package/src/components/Channel/hooks/useCreateChannelContext.ts b/package/src/components/Channel/hooks/useCreateChannelContext.ts
index b58b0dad60..d47c70fc5d 100644
--- a/package/src/components/Channel/hooks/useCreateChannelContext.ts
+++ b/package/src/components/Channel/hooks/useCreateChannelContext.ts
@@ -5,6 +5,7 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann
export const useCreateChannelContext = ({
channel,
channelUnreadState,
+ channelUnreadStateStore,
disabled,
EmptyStateIndicator,
enableMessageGroupingByUser,
@@ -46,12 +47,12 @@ export const useCreateChannelContext = ({
const readUsersLastReads = readUsers
.map(({ last_read }) => last_read?.toISOString() ?? '')
.join();
- const stringifiedChannelUnreadState = JSON.stringify(channelUnreadState);
const channelContext: ChannelContextValue = useMemo(
() => ({
channel,
channelUnreadState,
+ channelUnreadStateStore,
disabled,
EmptyStateIndicator,
enableMessageGroupingByUser,
@@ -96,7 +97,6 @@ export const useCreateChannelContext = ({
membersLength,
readUsersLength,
readUsersLastReads,
- stringifiedChannelUnreadState,
targetedMessage,
threadList,
watcherCount,
diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
index 3ac1e7236d..690a34a23d 100644
--- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts
+++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
@@ -27,6 +27,7 @@ export const useCreateMessagesContext = ({
FlatList,
forceAlignMessages,
Gallery,
+ getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
@@ -59,6 +60,7 @@ export const useCreateMessagesContext = ({
MessageActionListItem,
messageActions,
MessageAvatar,
+ MessageBlocked,
MessageBounce,
MessageContent,
messageContentOrder,
@@ -145,6 +147,7 @@ export const useCreateMessagesContext = ({
FlatList,
forceAlignMessages,
Gallery,
+ getMessageGroupStyle,
getMessagesGroupStyles,
Giphy,
giphyVersion,
@@ -177,6 +180,7 @@ export const useCreateMessagesContext = ({
MessageActionListItem,
messageActions,
MessageAvatar,
+ MessageBlocked,
MessageBounce,
MessageContent,
messageContentOrder,
diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx
index d1460cb61d..1949d3d6bb 100644
--- a/package/src/components/Message/Message.tsx
+++ b/package/src/components/Message/Message.tsx
@@ -180,6 +180,7 @@ export type MessagePropsWithContext = Pick<
| 'messageActions'
| 'messageContentOrder'
| 'MessageBounce'
+ | 'MessageBlocked'
| 'MessageSimple'
| 'onLongPressMessage'
| 'onPressInMessage'
@@ -252,10 +253,10 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
handleRetry,
handleThreadReply,
isTargetedMessage,
- lastReceivedId,
members,
message,
messageActions: messageActionsProp = defaultMessageActions,
+ MessageBlocked,
MessageBounce,
messageContentOrder: messageContentOrderProp,
MessageMenu,
@@ -650,7 +651,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
isMessageAIGenerated,
isMyMessage,
lastGroupMessage: groupStyles?.[0] === 'single' || groupStyles?.[0] === 'bottom',
- lastReceivedId,
members,
message,
messageContentOrder,
@@ -732,6 +732,10 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
return null;
}
+ if (isBlockedMessage(message)) {
+ return ;
+ }
+
return (
;
+};
+
+/**
+ * A component to display blocked message. e.g, when a message is blocked by moderation policies.
+ */
+export const MessageBlocked = (props: MessageBlockedProps) => {
+ const { message, style } = props;
+
+ const {
+ theme: {
+ colors: { grey, grey_whisper },
+ messageSimple: {
+ messageBlocked: { container, line, text, textContainer },
+ },
+ },
+ } = useTheme();
+
+ return (
+
+
+
+
+ {message.text?.toUpperCase() || ''}
+
+
+
+
+ );
+};
+
+MessageBlocked.displayName = 'MessageBlocked{messageList{messageBlocked}}';
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginBottom: 10,
+ },
+ line: {
+ flex: 1,
+ height: 0.5,
+ },
+ text: {
+ fontSize: 10,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ textContainer: {
+ flex: 3,
+ marginTop: 10,
+ },
+});
diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx
index 6c7ca849bd..aa7efb1af0 100644
--- a/package/src/components/Message/MessageSimple/MessageContent.tsx
+++ b/package/src/components/Message/MessageSimple/MessageContent.tsx
@@ -535,7 +535,6 @@ export const MessageContent = (props: MessageContentProps) => {
isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
- lastReceivedId,
message,
messageContentOrder,
onLongPress,
@@ -575,7 +574,6 @@ export const MessageContent = (props: MessageContentProps) => {
isEditedMessageOpen,
isMessageAIGenerated,
isMyMessage,
- lastReceivedId,
message,
messageContentOrder,
MessageError,
diff --git a/package/src/components/Message/MessageSimple/MessageWrapper.tsx b/package/src/components/Message/MessageSimple/MessageWrapper.tsx
new file mode 100644
index 0000000000..39a4a36f22
--- /dev/null
+++ b/package/src/components/Message/MessageSimple/MessageWrapper.tsx
@@ -0,0 +1,147 @@
+import React, { useCallback } from 'react';
+
+import { View } from 'react-native';
+
+import { LocalMessage } from 'stream-chat';
+
+import { useMessageDateSeparator } from '../../../components/MessageList/hooks/useMessageDateSeparator';
+import { useMessageGroupStyles } from '../../../components/MessageList/hooks/useMessageGroupStyles';
+import { useChannelContext } from '../../../contexts/channelContext/ChannelContext';
+import { useChatContext } from '../../../contexts/chatContext/ChatContext';
+import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext';
+import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
+import { ThemeProvider, useTheme } from '../../../contexts/themeContext/ThemeContext';
+
+import { useStateStore } from '../../../hooks/useStateStore';
+import { ChannelUnreadStateStoreType } from '../../../state-store/channel-unread-state';
+import { MessagePreviousAndNextMessageStoreType } from '../../../state-store/message-list-prev-next-state';
+
+const channelUnreadStateSelector = (state: ChannelUnreadStateStoreType) => ({
+ first_unread_message_id: state.channelUnreadState?.first_unread_message_id,
+ last_read_message_id: state.channelUnreadState?.last_read_message_id,
+ last_read_timestamp: state.channelUnreadState?.last_read?.getTime(),
+ unread_messages: state.channelUnreadState?.unread_messages,
+});
+
+export type MessageWrapperProps = {
+ message: LocalMessage;
+};
+
+export const MessageWrapper = React.memo((props: MessageWrapperProps) => {
+ const { message } = props;
+ const { client } = useChatContext();
+ const {
+ channelUnreadStateStore,
+ channel,
+ hideDateSeparators,
+ highlightedMessageId,
+ maxTimeBetweenGroupedMessages,
+ threadList,
+ } = useChannelContext();
+ const {
+ getMessageGroupStyle,
+ InlineDateSeparator,
+ InlineUnreadIndicator,
+ Message,
+ MessageSystem,
+ myMessageTheme,
+ shouldShowUnreadUnderlay,
+ } = useMessagesContext();
+ const {
+ goToMessage,
+ onThreadSelect,
+ noGroupByUser,
+ modifiedTheme,
+ messageListPreviousAndNextMessageStore,
+ } = useMessageListItemContext();
+
+ const dateSeparatorDate = useMessageDateSeparator({
+ hideDateSeparators,
+ message,
+ messageListPreviousAndNextMessageStore,
+ });
+
+ const selector = useCallback(
+ (state: MessagePreviousAndNextMessageStoreType) => ({
+ nextMessage: state.messageList[message.id]?.nextMessage,
+ }),
+ [message.id],
+ );
+ const { nextMessage } = useStateStore(messageListPreviousAndNextMessageStore.state, selector);
+ const isNewestMessage = nextMessage === undefined;
+ const groupStyles = useMessageGroupStyles({
+ dateSeparatorDate,
+ getMessageGroupStyle,
+ maxTimeBetweenGroupedMessages,
+ message,
+ messageListPreviousAndNextMessageStore,
+ noGroupByUser,
+ });
+
+ const { first_unread_message_id, last_read_timestamp, last_read_message_id, unread_messages } =
+ useStateStore(channelUnreadStateStore.state, channelUnreadStateSelector);
+ const {
+ theme: {
+ messageList: { messageContainer },
+ screenPadding,
+ },
+ } = useTheme();
+ if (!channel || channel.disconnected) {
+ return null;
+ }
+
+ const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
+ const isLastReadMessage =
+ last_read_message_id === message.id ||
+ (!unread_messages && createdAtTimestamp === last_read_timestamp);
+
+ const showUnreadSeparator =
+ isLastReadMessage &&
+ !isNewestMessage &&
+ // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label
+ (!!first_unread_message_id || !!unread_messages);
+
+ const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator;
+
+ const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme;
+ const renderDateSeperator = dateSeparatorDate ? (
+
+ ) : null;
+
+ const renderMessage = (
+
+ );
+
+ return (
+
+ {message.type === 'system' ? (
+
+ ) : wrapMessageInTheme ? (
+
+
+ {renderDateSeperator}
+ {renderMessage}
+
+
+ ) : (
+
+ {renderDateSeperator}
+ {renderMessage}
+
+ )}
+ {showUnreadUnderlay && }
+
+ );
+});
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
index 36958bfe38..e741c75844 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
@@ -81,11 +81,7 @@ describe('MessageStatus', () => {
-
+
,
diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts
index d2bb4d994f..b44fa768e5 100644
--- a/package/src/components/Message/hooks/useCreateMessageContext.ts
+++ b/package/src/components/Message/hooks/useCreateMessageContext.ts
@@ -22,7 +22,6 @@ export const useCreateMessageContext = ({
isMessageAIGenerated,
isMyMessage,
lastGroupMessage,
- lastReceivedId,
members,
message,
messageContentOrder,
@@ -74,7 +73,6 @@ export const useCreateMessageContext = ({
isMessageAIGenerated,
isMyMessage,
lastGroupMessage,
- lastReceivedId,
members,
message,
messageContentOrder,
@@ -105,7 +103,6 @@ export const useCreateMessageContext = ({
hasReactions,
isEditedMessageOpen,
lastGroupMessage,
- lastReceivedId,
membersValue,
myMessageThemeString,
reactionsValue,
diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx
index efd775cd99..842a3f355a 100644
--- a/package/src/components/MessageList/MessageFlashList.tsx
+++ b/package/src/components/MessageList/MessageFlashList.tsx
@@ -17,8 +17,6 @@ import { InlineLoadingMoreIndicator } from './InlineLoadingMoreIndicator';
import { InlineLoadingMoreRecentIndicator } from './InlineLoadingMoreRecentIndicator';
import { InlineLoadingMoreRecentThreadIndicator } from './InlineLoadingMoreRecentThreadIndicator';
-import { getLastReceivedMessageFlashList } from './utils/getLastReceivedMessageFlashList';
-
import {
AttachmentPickerContextValue,
useAttachmentPickerContext,
@@ -32,6 +30,10 @@ import {
ImageGalleryContextValue,
useImageGalleryContext,
} from '../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ MessageListItemContextValue,
+ MessageListItemProvider,
+} from '../../contexts/messageListItemContext/MessageListItemContext';
import {
MessagesContextValue,
useMessagesContext,
@@ -44,11 +46,12 @@ import {
PaginatedMessageListContextValue,
usePaginatedMessageListContext,
} from '../../contexts/paginatedMessageListContext/PaginatedMessageListContext';
-import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext';
+import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext';
import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext';
import { useStableCallback } from '../../hooks';
import { FileTypes } from '../../types/types';
+import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper';
let FlashList;
@@ -102,6 +105,7 @@ type MessageFlashListPropsWithContext = Pick<
ChannelContextValue,
| 'channel'
| 'channelUnreadState'
+ | 'channelUnreadStateStore'
| 'disabled'
| 'EmptyStateIndicator'
| 'hideStickyDateHeader'
@@ -250,6 +254,10 @@ const getItemTypeInternal = (message: LocalMessage) => {
return 'generic-message';
};
+const renderItem = ({ item: message }: { item: LocalMessage }) => {
+ return ;
+};
+
const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) => {
const LoadingMoreRecentIndicator = props.threadList
? InlineLoadingMoreRecentThreadIndicator
@@ -257,7 +265,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const {
additionalFlashListProps,
channel,
- channelUnreadState,
+ channelUnreadStateStore,
client,
closePicker,
DateHeader,
@@ -268,9 +276,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
FooterComponent = LoadingMoreRecentIndicator,
HeaderComponent = InlineLoadingMoreIndicator,
hideStickyDateHeader,
- highlightedMessageId,
- InlineDateSeparator,
- InlineUnreadIndicator,
isListActive = false,
isLiveStreaming = false,
legacyImageViewerSwipeBehaviour,
@@ -283,8 +288,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
loadMoreThread,
markRead,
maximumMessageLimit,
- Message,
- MessageSystem,
myMessageTheme,
readEvents,
NetworkDownIndicator,
@@ -299,7 +302,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
setMessages,
setSelectedPicker,
setTargetedMessage,
- shouldShowUnreadUnderlay,
StickyHeader,
targetedMessage,
thread,
@@ -349,8 +351,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
);
const {
- dateSeparatorsRef,
- messageGroupStylesRef,
+ messageListPreviousAndNextMessageStore,
processedMessageList,
rawMessageList,
viewabilityChangedCallback,
@@ -378,11 +379,6 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
client.userID,
);
- const lastReceivedId = useMemo(
- () => getLastReceivedMessageFlashList(processedMessageList)?.id,
- [processedMessageList],
- );
-
const [autoscrollToRecent, setAutoscrollToRecent] = useState(true);
useEffect(() => {
@@ -558,6 +554,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
*/
useEffect(() => {
const shouldMarkRead = () => {
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
return (
!channelUnreadState?.first_unread_message_id &&
!scrollToBottomButtonVisible &&
@@ -569,23 +566,22 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const handleEvent = async (event: Event) => {
const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
const isMyOwnMessage = event.message?.user?.id === client.user?.id;
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
// When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState when its a received message.
if (
(scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) &&
!isMyOwnMessage
) {
- setChannelUnreadState((prev) => {
- const previousUnreadCount = prev?.unread_messages ?? 0;
- const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message);
- return {
- ...(prev || {}),
- last_read:
- prev?.last_read ??
- (previousUnreadCount === 0 && previousLastMessage?.created_at
- ? new Date(previousLastMessage.created_at)
- : new Date(0)), // not having information about the last read message means the whole channel is unread,
- unread_messages: previousUnreadCount + 1,
- };
+ const previousUnreadCount = channelUnreadState?.unread_messages ?? 0;
+ const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message);
+ setChannelUnreadState({
+ ...channelUnreadState,
+ last_read:
+ channelUnreadState?.last_read ??
+ (previousUnreadCount === 0 && previousLastMessage?.created_at
+ ? new Date(previousLastMessage.created_at)
+ : new Date(0)), // not having information about the last read message means the whole channel is unread,
+ unread_messages: previousUnreadCount + 1,
});
} else if (mainChannelUpdated && shouldMarkRead()) {
await markRead();
@@ -599,7 +595,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
};
}, [
channel,
- channelUnreadState?.first_unread_message_id,
+ channelUnreadStateStore,
client.user?.id,
markRead,
scrollToBottomButtonVisible,
@@ -640,12 +636,19 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
* This function should show or hide the unread indicator depending on the
*/
const updateStickyUnreadIndicator = useStableCallback((viewableItems: ViewToken[]) => {
- if (!viewableItems.length || !readEvents) {
- setIsUnreadNotificationOpen(false);
- return;
- }
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
+ // we need this check to make sure that regular list change do not trigger
+ // the unread notification to appear (for example if the old last read messages
+ // go out of the viewport).
+ const lastReadMessageId = channelUnreadState?.last_read_message_id;
+ const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId);
- if (selectedPicker === 'images') {
+ if (
+ !viewableItems.length ||
+ !readEvents ||
+ lastReadMessageVisible ||
+ selectedPicker === 'images'
+ ) {
setIsUnreadNotificationOpen(false);
return;
}
@@ -657,7 +660,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const lastItemMessage = lastItem.item;
const lastItemCreatedAt = lastItemMessage.created_at;
- const unreadIndicatorDate = channelUnreadState?.last_read.getTime();
+ const unreadIndicatorDate = channelUnreadState?.last_read?.getTime();
const lastItemDate = lastItemCreatedAt.getTime();
if (
@@ -716,89 +719,20 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
[],
);
- const renderItem = useCallback(
- ({ index, item: message }: { index: number; item: LocalMessage }) => {
- if (!channel || channel.disconnected) {
- return null;
- }
-
- const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
- const lastReadTimestamp = channelUnreadState?.last_read.getTime();
- const isNewestMessage = index === 0;
- const isLastReadMessage =
- channelUnreadState?.last_read_message_id === message.id ||
- (!channelUnreadState?.unread_messages && createdAtTimestamp === lastReadTimestamp);
-
- const showUnreadSeparator =
- isLastReadMessage &&
- !isNewestMessage &&
- // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label
- (!!channelUnreadState?.first_unread_message_id || !!channelUnreadState?.unread_messages);
-
- const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator;
-
- const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme;
- const renderDateSeperator = dateSeparatorsRef.current[message.id] && (
-
- );
-
- const renderMessage = (
-
- );
-
- return (
-
- {message.type === 'system' ? (
-
- ) : wrapMessageInTheme ? (
-
-
- {renderDateSeperator}
- {renderMessage}
-
-
- ) : (
-
- {renderDateSeperator}
- {renderMessage}
-
- )}
- {showUnreadUnderlay && }
-
- );
- },
+ const messageListItemContextValue: MessageListItemContextValue = useMemo(
+ () => ({
+ goToMessage,
+ messageListPreviousAndNextMessageStore,
+ modifiedTheme,
+ noGroupByUser,
+ onThreadSelect,
+ }),
[
- InlineDateSeparator,
- InlineUnreadIndicator,
- Message,
- MessageSystem,
- channel,
- channelUnreadState?.first_unread_message_id,
- channelUnreadState?.last_read,
- channelUnreadState?.last_read_message_id,
- channelUnreadState?.unread_messages,
- client.userID,
- dateSeparatorsRef,
goToMessage,
- highlightedMessageId,
- lastReceivedId,
- messageGroupStylesRef,
+ messageListPreviousAndNextMessageStore,
modifiedTheme,
- myMessageTheme,
+ noGroupByUser,
onThreadSelect,
- shouldShowUnreadUnderlay,
- threadList,
],
);
@@ -1129,34 +1063,36 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
{EmptyStateIndicator ? : null}
) : (
-
+
+
+
)}
{messageListLengthAfterUpdate && StickyHeader ? (
@@ -1194,6 +1130,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
const {
channel,
channelUnreadState,
+ channelUnreadStateStore,
disabled,
EmptyStateIndicator,
enableMessageGroupingByUser,
@@ -1242,6 +1179,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
{...{
channel,
channelUnreadState,
+ channelUnreadStateStore,
client,
closePicker,
DateHeader,
diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx
index d6ed5a9285..a86fd62e47 100644
--- a/package/src/components/MessageList/MessageList.tsx
+++ b/package/src/components/MessageList/MessageList.tsx
@@ -17,7 +17,6 @@ import { useShouldScrollToRecentOnNewOwnMessage } from './hooks/useShouldScrollT
import { InlineLoadingMoreIndicator } from './InlineLoadingMoreIndicator';
import { InlineLoadingMoreRecentIndicator } from './InlineLoadingMoreRecentIndicator';
import { InlineLoadingMoreRecentThreadIndicator } from './InlineLoadingMoreRecentThreadIndicator';
-import { getLastReceivedMessage } from './utils/getLastReceivedMessage';
import {
AttachmentPickerContextValue,
@@ -33,6 +32,10 @@ import {
ImageGalleryContextValue,
useImageGalleryContext,
} from '../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ MessageListItemContextValue,
+ MessageListItemProvider,
+} from '../../contexts/messageListItemContext/MessageListItemContext';
import {
MessagesContextValue,
useMessagesContext,
@@ -45,11 +48,12 @@ import {
PaginatedMessageListContextValue,
usePaginatedMessageListContext,
} from '../../contexts/paginatedMessageListContext/PaginatedMessageListContext';
-import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext';
+import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext';
import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext';
import { useStableCallback } from '../../hooks';
import { FileTypes } from '../../types/types';
+import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper';
// This is just to make sure that the scrolling happens in a different task queue.
// TODO: Think if we really need this and strive to remove it if we can.
@@ -124,10 +128,10 @@ type MessageListPropsWithContext = Pick<
ChannelContextValue,
| 'channel'
| 'channelUnreadState'
+ | 'channelUnreadStateStore'
| 'disabled'
| 'EmptyStateIndicator'
| 'hideStickyDateHeader'
- | 'highlightedMessageId'
| 'loadChannelAroundMessage'
| 'loading'
| 'LoadingIndicator'
@@ -150,14 +154,9 @@ type MessageListPropsWithContext = Pick<
| 'DateHeader'
| 'disableTypingIndicator'
| 'FlatList'
- | 'InlineDateSeparator'
- | 'InlineUnreadIndicator'
| 'legacyImageViewerSwipeBehaviour'
- | 'Message'
| 'ScrollToBottomButton'
- | 'MessageSystem'
| 'myMessageTheme'
- | 'shouldShowUnreadUnderlay'
| 'TypingIndicator'
| 'TypingIndicatorContainer'
| 'UnreadMessagesNotification'
@@ -234,6 +233,10 @@ type MessageListPropsWithContext = Pick<
isLiveStreaming?: boolean;
};
+const renderItem = ({ item: message }: { item: LocalMessage }) => {
+ return ;
+};
+
/**
* The message list component renders a list of messages. It consumes the following contexts:
*
@@ -250,7 +253,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const {
additionalFlatListProps,
channel,
- channelUnreadState,
+ channelUnreadStateStore,
client,
closePicker,
DateHeader,
@@ -261,9 +264,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
FooterComponent = InlineLoadingMoreIndicator,
HeaderComponent = LoadingMoreRecentIndicator,
hideStickyDateHeader,
- highlightedMessageId,
- InlineDateSeparator,
- InlineUnreadIndicator,
inverted = true,
isListActive = false,
isLiveStreaming = false,
@@ -277,8 +277,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
loadMoreThread,
markRead,
maximumMessageLimit,
- Message,
- MessageSystem,
myMessageTheme,
NetworkDownIndicator,
noGroupByUser,
@@ -293,7 +291,6 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
setMessages,
setSelectedPicker,
setTargetedMessage,
- shouldShowUnreadUnderlay,
StickyHeader,
targetedMessage,
thread,
@@ -308,8 +305,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const {
colors: { white_snow },
- messageList: { container, contentContainer, listContainer, messageContainer },
- screenPadding,
+ messageList: { container, contentContainer, listContainer },
} = theme;
const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]);
@@ -325,8 +321,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
* processedMessageList changes on any state change
*/
const {
- dateSeparatorsRef,
- messageGroupStylesRef,
+ messageListPreviousAndNextMessageStore,
processedMessageList,
rawMessageList,
viewabilityChangedCallback,
@@ -395,10 +390,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
*/
const messageIdLastScrolledToRef = useRef(undefined);
const [hasMoved, setHasMoved] = useState(false);
- const lastReceivedId = useMemo(
- () => getLastReceivedMessage(processedMessageList)?.id,
- [processedMessageList],
- );
+
const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false);
const [stickyHeaderDate, setStickyHeaderDate] = useState();
@@ -437,26 +429,23 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
}
});
- const messagesLength = useRef(processedMessageList.length);
-
/**
* This function should show or hide the unread indicator depending on the
*/
const updateStickyUnreadIndicator = useStableCallback((viewableItems: ViewToken[]) => {
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
// we need this check to make sure that regular list change do not trigger
// the unread notification to appear (for example if the old last read messages
// go out of the viewport).
- if (processedMessageList.length !== messagesLength.current) {
- return;
- }
- messagesLength.current = processedMessageList.length;
+ const lastReadMessageId = channelUnreadState?.last_read_message_id;
+ const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId);
- if (!viewableItems.length || !readEvents) {
- setIsUnreadNotificationOpen(false);
- return;
- }
-
- if (selectedPicker === 'images') {
+ if (
+ !viewableItems.length ||
+ !readEvents ||
+ lastReadMessageVisible ||
+ selectedPicker === 'images'
+ ) {
setIsUnreadNotificationOpen(false);
return;
}
@@ -467,7 +456,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const lastItemMessage = lastItem.item;
const lastItemCreatedAt = lastItemMessage.created_at;
- const unreadIndicatorDate = channelUnreadState?.last_read.getTime();
+ const unreadIndicatorDate = channelUnreadState?.last_read?.getTime();
const lastItemDate = lastItemCreatedAt.getTime();
if (
@@ -547,6 +536,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
*/
useEffect(() => {
const shouldMarkRead = () => {
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
return (
!channelUnreadState?.first_unread_message_id &&
!scrollToBottomButtonVisible &&
@@ -558,23 +548,22 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const handleEvent = async (event: Event) => {
const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
const isMyOwnMessage = event.message?.user?.id === client.user?.id;
+ const channelUnreadState = channelUnreadStateStore.channelUnreadState;
// When the scrollToBottomButtonVisible is true, we need to manually update the channelUnreadState when its a received message.
if (
(scrollToBottomButtonVisible || channelUnreadState?.first_unread_message_id) &&
!isMyOwnMessage
) {
- setChannelUnreadState((prev) => {
- const previousUnreadCount = prev?.unread_messages ?? 0;
- const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message);
- return {
- ...(prev || {}),
- last_read:
- prev?.last_read ??
- (previousUnreadCount === 0 && previousLastMessage?.created_at
- ? new Date(previousLastMessage.created_at)
- : new Date(0)), // not having information about the last read message means the whole channel is unread,
- unread_messages: previousUnreadCount + 1,
- };
+ const previousUnreadCount = channelUnreadState?.unread_messages ?? 0;
+ const previousLastMessage = getPreviousLastMessage(channel.state.messages, event.message);
+ setChannelUnreadState({
+ ...channelUnreadState,
+ last_read:
+ channelUnreadState?.last_read ??
+ (previousUnreadCount === 0 && previousLastMessage?.created_at
+ ? new Date(previousLastMessage.created_at)
+ : new Date(0)), // not having information about the last read message means the whole channel is unread,
+ unread_messages: previousUnreadCount + 1,
});
} else if (mainChannelUpdated && shouldMarkRead()) {
await markRead();
@@ -588,7 +577,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
};
}, [
channel,
- channelUnreadState?.first_unread_message_id,
+ channelUnreadStateStore,
client.user?.id,
markRead,
scrollToBottomButtonVisible,
@@ -779,95 +768,20 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetedMessage]);
- const renderItem = useCallback(
- ({ index, item: message }: { index: number; item: LocalMessage }) => {
- if (!channel || channel.disconnected) {
- return null;
- }
-
- const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
- const lastReadTimestamp = channelUnreadState?.last_read.getTime();
- const isNewestMessage = index === 0;
- const isLastReadMessage =
- channelUnreadState?.last_read_message_id === message.id ||
- (!channelUnreadState?.unread_messages && createdAtTimestamp === lastReadTimestamp);
-
- const showUnreadSeparator =
- isLastReadMessage &&
- !isNewestMessage &&
- // The `channelUnreadState?.first_unread_message_id` is here for sent messages unread label
- (!!channelUnreadState?.first_unread_message_id || !!channelUnreadState?.unread_messages);
-
- const showUnreadUnderlay = !!shouldShowUnreadUnderlay && showUnreadSeparator;
-
- const wrapMessageInTheme = client.userID === message.user?.id && !!myMessageTheme;
- const renderDateSeperator = dateSeparatorsRef.current[message.id] && (
-
- );
-
- const renderMessage = (
-
- );
-
- return (
-
- {message.type === 'system' ? (
-
- ) : wrapMessageInTheme ? (
-
-
- {renderDateSeperator}
- {renderMessage}
-
-
- ) : (
-
- {renderDateSeperator}
- {renderMessage}
-
- )}
- {showUnreadUnderlay && }
-
- );
- },
+ const messageListItemContextValue: MessageListItemContextValue = useMemo(
+ () => ({
+ goToMessage,
+ messageListPreviousAndNextMessageStore,
+ modifiedTheme,
+ noGroupByUser,
+ onThreadSelect,
+ }),
[
- InlineDateSeparator,
- InlineUnreadIndicator,
- Message,
- MessageSystem,
- channel,
- channelUnreadState?.first_unread_message_id,
- channelUnreadState?.last_read,
- channelUnreadState?.last_read_message_id,
- channelUnreadState?.unread_messages,
- client.userID,
- dateSeparatorsRef,
goToMessage,
- highlightedMessageId,
- lastReceivedId,
- messageContainer,
- messageGroupStylesRef,
+ messageListPreviousAndNextMessageStore,
modifiedTheme,
- myMessageTheme,
+ noGroupByUser,
onThreadSelect,
- screenPadding,
- shouldShowUnreadUnderlay,
- threadList,
],
);
@@ -1232,42 +1146,44 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
{EmptyStateIndicator ? : null}
) : (
-
+
+ maintainVisibleContentPosition={maintainVisibleContentPosition}
+ maxToRenderPerBatch={30}
+ onMomentumScrollEnd={onUserScrollEvent}
+ onScroll={handleScroll}
+ onScrollBeginDrag={onScrollBeginDrag}
+ onScrollEndDrag={onScrollEndDrag}
+ onScrollToIndexFailed={onScrollToIndexFailedRef.current}
+ onTouchEnd={dismissImagePicker}
+ onViewableItemsChanged={stableOnViewableItemsChanged}
+ ref={refCallback}
+ renderItem={renderItem}
+ scrollEventThrottle={isLiveStreaming ? 16 : undefined}
+ showsVerticalScrollIndicator={false}
+ // @ts-expect-error react-native internal
+ strictMode={isLiveStreaming}
+ style={flatListStyle}
+ testID='message-flat-list'
+ viewabilityConfig={flatListViewabilityConfig}
+ {...additionalFlatListPropsExcludingStyle}
+ />
+
)}
{messageListLengthAfterUpdate && StickyHeader ? (
@@ -1299,6 +1215,7 @@ export const MessageList = (props: MessageListProps) => {
const {
channel,
channelUnreadState,
+ channelUnreadStateStore,
disabled,
EmptyStateIndicator,
enableMessageGroupingByUser,
@@ -1347,6 +1264,7 @@ export const MessageList = (props: MessageListProps) => {
{...{
channel,
channelUnreadState,
+ channelUnreadStateStore,
client,
closePicker,
DateHeader,
diff --git a/package/src/components/MessageList/UnreadMessagesNotification.tsx b/package/src/components/MessageList/UnreadMessagesNotification.tsx
index bca539e3be..47192ee192 100644
--- a/package/src/components/MessageList/UnreadMessagesNotification.tsx
+++ b/package/src/components/MessageList/UnreadMessagesNotification.tsx
@@ -21,7 +21,7 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp
const { onCloseHandler, onPressHandler } = props;
const { t } = useTranslationContext();
const {
- channelUnreadState,
+ channelUnreadStateStore,
loadChannelAtFirstUnreadMessage,
markRead,
setChannelUnreadState,
@@ -33,7 +33,7 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp
await onPressHandler();
} else {
await loadChannelAtFirstUnreadMessage({
- channelUnreadState,
+ channelUnreadState: channelUnreadStateStore.channelUnreadState,
setChannelUnreadState,
setTargetedMessage,
});
diff --git a/package/src/components/MessageList/__tests__/MessageList.test.js b/package/src/components/MessageList/__tests__/MessageList.test.js
index 116c36a57d..066d148c74 100644
--- a/package/src/components/MessageList/__tests__/MessageList.test.js
+++ b/package/src/components/MessageList/__tests__/MessageList.test.js
@@ -382,12 +382,19 @@ describe('MessageList', () => {
});
});
- it("should render the UnreadMessagesIndicator when there's unread messages", async () => {
+ it("should render the InlineUnreadIndicator when there's unread messages", async () => {
const user1 = generateUser();
const user2 = generateUser();
const messages = Array.from({ length: 10 }, (_, i) =>
generateMessage({ id: `${i}`, text: `message-${i}` }),
);
+ const read_data = {
+ [user1.id]: {
+ last_read: new Date(),
+ last_read_message_id: '5',
+ unread_messages: 5,
+ },
+ };
const mockedChannel = generateChannelResponse({
members: [generateMember({ user: user1 }), generateMember({ user: user2 })],
});
@@ -397,23 +404,18 @@ describe('MessageList', () => {
const channel = chatClient.channel('messaging', mockedChannel.id);
await channel.watch();
- const channelUnreadState = {
- last_read: new Date(),
- last_read_message_id: '5',
- unread_messages: 5,
- };
-
channel.state = {
...channelInitialState,
latestMessages: [],
messages,
+ read: read_data,
};
const { queryByLabelText } = render(
-
+
,
diff --git a/package/src/components/MessageList/__tests__/useMessageDateSeparator.test.ts b/package/src/components/MessageList/__tests__/useMessageDateSeparator.test.ts
new file mode 100644
index 0000000000..07203353a6
--- /dev/null
+++ b/package/src/components/MessageList/__tests__/useMessageDateSeparator.test.ts
@@ -0,0 +1,83 @@
+import { renderHook } from '@testing-library/react-native';
+
+import { LocalMessage } from 'stream-chat';
+
+import { MessagePreviousAndNextMessageStore } from '../../../state-store/message-list-prev-next-state';
+import { useMessageDateSeparator } from '../hooks/useMessageDateSeparator';
+
+describe('useMessageDateSeparator', () => {
+ let messageListPreviousAndNextMessageStore: MessagePreviousAndNextMessageStore;
+ let messages: LocalMessage[];
+
+ beforeEach(() => {
+ messageListPreviousAndNextMessageStore = new MessagePreviousAndNextMessageStore();
+ messages = [
+ {
+ created_at: new Date('2020-01-01T00:00:00.000Z'),
+ id: '1',
+ text: 'Hello',
+ },
+ {
+ created_at: new Date('2020-01-02T00:00:00.000Z'),
+ id: '2',
+ text: 'World',
+ },
+ {
+ created_at: new Date('2020-01-03T00:00:00.000Z'),
+ id: '3',
+ text: 'Hello World',
+ },
+ ] as LocalMessage[];
+ messageListPreviousAndNextMessageStore.setMessageListPreviousAndNextMessage({ messages });
+ });
+
+ it('should return undefined if no message is passed', () => {
+ const { result } = renderHook(() =>
+ useMessageDateSeparator({ message: undefined, messageListPreviousAndNextMessageStore }),
+ );
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return undefined if the hideDateSeparators prop is true', () => {
+ const { result } = renderHook(() =>
+ useMessageDateSeparator({
+ hideDateSeparators: true,
+ message: messages[1],
+ messageListPreviousAndNextMessageStore,
+ }),
+ );
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return the date separator for a message if previous message is not the same day', () => {
+ const { result } = renderHook(() =>
+ useMessageDateSeparator({ message: messages[1], messageListPreviousAndNextMessageStore }),
+ );
+ expect(result.current).toBe(messages[1].created_at);
+ });
+
+ it('should return undefined if the message is the same day as the previous message', () => {
+ const messages = [
+ {
+ created_at: new Date('2020-01-01T01:00:00.000Z'),
+ id: '1',
+ text: 'Hello',
+ },
+ {
+ created_at: new Date('2020-01-01T02:00:00.000Z'),
+ id: '2',
+ text: 'World',
+ },
+ ] as LocalMessage[];
+ const messageListPreviousAndNextMessageStore = new MessagePreviousAndNextMessageStore();
+ messageListPreviousAndNextMessageStore.setMessageListPreviousAndNextMessage({ messages });
+ const { result: resultOfFirstMessage } = renderHook(() =>
+ useMessageDateSeparator({ message: messages[0], messageListPreviousAndNextMessageStore }),
+ );
+ expect(resultOfFirstMessage.current).toBe(messages[0].created_at);
+ const { result: resultOfSecondMessage } = renderHook(() =>
+ useMessageDateSeparator({ message: messages[1], messageListPreviousAndNextMessageStore }),
+ );
+ expect(resultOfSecondMessage.current).toBeUndefined();
+ });
+});
diff --git a/package/src/components/MessageList/hooks/useMessageDateSeparator.ts b/package/src/components/MessageList/hooks/useMessageDateSeparator.ts
new file mode 100644
index 0000000000..80957b0b1a
--- /dev/null
+++ b/package/src/components/MessageList/hooks/useMessageDateSeparator.ts
@@ -0,0 +1,66 @@
+import { useCallback, useMemo } from 'react';
+
+import { LocalMessage } from 'stream-chat';
+
+import { useStateStore } from '../../../hooks/useStateStore';
+import {
+ MessagePreviousAndNextMessageStore,
+ MessagePreviousAndNextMessageStoreType,
+} from '../../../state-store/message-list-prev-next-state';
+
+export const getDateSeparatorValue = ({
+ hideDateSeparators,
+ message,
+ previousMessage,
+}: {
+ hideDateSeparators?: boolean;
+ message?: LocalMessage;
+ previousMessage?: LocalMessage;
+}) => {
+ if (hideDateSeparators) {
+ return undefined;
+ }
+
+ const previousMessageDate = previousMessage?.created_at.toDateString();
+ const messageDate = message?.created_at.toDateString();
+
+ if (previousMessageDate !== messageDate) {
+ return message?.created_at;
+ }
+
+ return undefined;
+};
+
+/**
+ * Hook to get whether a message should have a date separator above it
+ */
+export const useMessageDateSeparator = ({
+ hideDateSeparators,
+ message,
+ messageListPreviousAndNextMessageStore,
+}: {
+ hideDateSeparators?: boolean;
+ message?: LocalMessage;
+ messageListPreviousAndNextMessageStore: MessagePreviousAndNextMessageStore;
+}) => {
+ const selector = useCallback(
+ (state: MessagePreviousAndNextMessageStoreType) => ({
+ previousMessage: message ? state.messageList[message.id]?.previousMessage : undefined,
+ }),
+ [message],
+ );
+ const { previousMessage } = useStateStore(messageListPreviousAndNextMessageStore.state, selector);
+
+ const dateSeparatorDate = useMemo(() => {
+ if (!message && !previousMessage) {
+ return undefined;
+ }
+ return getDateSeparatorValue({
+ hideDateSeparators,
+ message,
+ previousMessage,
+ });
+ }, [hideDateSeparators, message, previousMessage]);
+
+ return dateSeparatorDate;
+};
diff --git a/package/src/components/MessageList/hooks/useMessageGroupStyles.ts b/package/src/components/MessageList/hooks/useMessageGroupStyles.ts
new file mode 100644
index 0000000000..4d4d286bc8
--- /dev/null
+++ b/package/src/components/MessageList/hooks/useMessageGroupStyles.ts
@@ -0,0 +1,75 @@
+import { useCallback, useMemo } from 'react';
+
+import { LocalMessage } from 'stream-chat';
+
+import { useMessageDateSeparator } from './useMessageDateSeparator';
+
+import { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
+import { useStateStore } from '../../../hooks/useStateStore';
+import {
+ MessagePreviousAndNextMessageStore,
+ MessagePreviousAndNextMessageStoreType,
+} from '../../../state-store/message-list-prev-next-state';
+import { getGroupStyle } from '../utils/getGroupStyles';
+
+/**
+ * Hook to get the group styles for a message
+ */
+export const useMessageGroupStyles = ({
+ noGroupByUser,
+ dateSeparatorDate,
+ maxTimeBetweenGroupedMessages,
+ message,
+ messageListPreviousAndNextMessageStore,
+ getMessageGroupStyle = getGroupStyle,
+}: {
+ noGroupByUser?: boolean;
+ getMessageGroupStyle: MessagesContextValue['getMessageGroupStyle'];
+ dateSeparatorDate?: Date;
+ maxTimeBetweenGroupedMessages?: number;
+ message: LocalMessage;
+ messageListPreviousAndNextMessageStore: MessagePreviousAndNextMessageStore;
+}) => {
+ const selector = useCallback(
+ (state: MessagePreviousAndNextMessageStoreType) => ({
+ nextMessage: state.messageList[message.id]?.nextMessage,
+ previousMessage: state.messageList[message.id]?.previousMessage,
+ }),
+ [message.id],
+ );
+ const { previousMessage, nextMessage } = useStateStore(
+ messageListPreviousAndNextMessageStore.state,
+ selector,
+ );
+
+ // This is needed to calculate the group styles for the next message
+ const nextMessageDateSeparatorDate = useMessageDateSeparator({
+ message: nextMessage,
+ messageListPreviousAndNextMessageStore,
+ });
+
+ const groupStyles = useMemo(() => {
+ if (noGroupByUser) {
+ return [];
+ }
+ return getMessageGroupStyle({
+ dateSeparatorDate,
+ maxTimeBetweenGroupedMessages,
+ message,
+ nextMessage,
+ nextMessageDateSeparatorDate,
+ previousMessage,
+ });
+ }, [
+ noGroupByUser,
+ getMessageGroupStyle,
+ dateSeparatorDate,
+ maxTimeBetweenGroupedMessages,
+ message,
+ nextMessage,
+ nextMessageDateSeparatorDate,
+ previousMessage,
+ ]);
+
+ return groupStyles;
+};
diff --git a/package/src/components/MessageList/hooks/useMessageList.ts b/package/src/components/MessageList/hooks/useMessageList.ts
index 01969ce221..75366278ea 100644
--- a/package/src/components/MessageList/hooks/useMessageList.ts
+++ b/package/src/components/MessageList/hooks/useMessageList.ts
@@ -1,4 +1,4 @@
-import { useMemo, useRef } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import type { LocalMessage } from 'stream-chat';
@@ -12,17 +12,24 @@ import { usePaginatedMessageListContext } from '../../../contexts/paginatedMessa
import { useThreadContext } from '../../../contexts/threadContext/ThreadContext';
import { useRAFCoalescedValue } from '../../../hooks';
+import { MessagePreviousAndNextMessageStore } from '../../../state-store/message-list-prev-next-state';
import { DateSeparators, getDateSeparators } from '../utils/getDateSeparators';
import { getGroupStyles } from '../utils/getGroupStyles';
export type UseMessageListParams = {
deletedMessagesVisibilityType?: DeletedMessagesVisibilityType;
+ /**
+ * @deprecated
+ */
noGroupByUser?: boolean;
threadList?: boolean;
isLiveStreaming?: boolean;
isFlashList?: boolean;
};
+/**
+ * FIXME: To change it to a more specific type.
+ */
export type GroupType = string;
export type MessageGroupStyles = {
@@ -35,23 +42,27 @@ export const shouldIncludeMessageInList = (
) => {
const { deletedMessagesVisibilityType, userId } = options;
const isMessageTypeDeleted = message.type === 'deleted';
+ const isSender = message.user?.id === userId;
+
+ if (!isMessageTypeDeleted) {
+ return true;
+ }
+
switch (deletedMessagesVisibilityType) {
+ case 'always':
+ return true;
case 'sender':
- return !isMessageTypeDeleted || message.user?.id === userId;
-
+ return isSender;
case 'receiver':
- return !isMessageTypeDeleted || message.user?.id !== userId;
-
+ return !isSender;
case 'never':
- return !isMessageTypeDeleted;
-
default:
- return !!message;
+ return false;
}
};
export const useMessageList = (params: UseMessageListParams) => {
- const { noGroupByUser, threadList, isLiveStreaming, isFlashList } = params;
+ const { noGroupByUser, threadList, isLiveStreaming, isFlashList = false } = params;
const { client } = useChatContext();
const { hideDateSeparators, maxTimeBetweenGroupedMessages } = useChannelContext();
const { deletedMessagesVisibilityType, getMessagesGroupStyles = getGroupStyles } =
@@ -59,64 +70,84 @@ export const useMessageList = (params: UseMessageListParams) => {
const { messages, viewabilityChangedCallback } = usePaginatedMessageListContext();
const { threadMessages } = useThreadContext();
const messageList = threadList ? threadMessages : messages;
+ const [messageListPreviousAndNextMessageStore] = useState(
+ () => new MessagePreviousAndNextMessageStore(),
+ );
+ const processedMessageList = useMemo(() => {
+ const newMessageList = [];
+ for (const message of messageList) {
+ if (
+ !shouldIncludeMessageInList(message, {
+ deletedMessagesVisibilityType,
+ userId: client.userID,
+ })
+ ) {
+ continue;
+ }
+ if (isFlashList) {
+ newMessageList.push(message);
+ } else {
+ newMessageList.unshift(message);
+ }
+ }
+ return newMessageList;
+ }, [messageList, deletedMessagesVisibilityType, client.userID, isFlashList]);
+
+ useEffect(() => {
+ messageListPreviousAndNextMessageStore.setMessageListPreviousAndNextMessage({
+ isFlashList,
+ messages: processedMessageList,
+ });
+ }, [processedMessageList, messageListPreviousAndNextMessageStore, isFlashList]);
+
+ /**
+ * @deprecated use `useDateSeparator` hook instead directly in the Message.
+ */
const dateSeparators = useMemo(
() =>
getDateSeparators({
- deletedMessagesVisibilityType,
hideDateSeparators,
- messages: messageList,
- userId: client.userID,
+ messages: processedMessageList,
}),
- [deletedMessagesVisibilityType, hideDateSeparators, messageList, client.userID],
+ [hideDateSeparators, processedMessageList],
);
+ /**
+ * @deprecated use `useDateSeparator` hook instead directly in the Message.
+ */
const dateSeparatorsRef = useRef(dateSeparators);
dateSeparatorsRef.current = dateSeparators;
+ /**
+ * @deprecated use `useMessageGroupStyles` hook instead directly in the Message.
+ */
const messageGroupStyles = useMemo(
() =>
getMessagesGroupStyles({
dateSeparators: dateSeparatorsRef.current,
hideDateSeparators,
maxTimeBetweenGroupedMessages,
- messages: messageList,
+ messages: processedMessageList,
noGroupByUser,
userId: client.userID,
}),
[
- dateSeparatorsRef,
getMessagesGroupStyles,
hideDateSeparators,
maxTimeBetweenGroupedMessages,
- messageList,
+ processedMessageList,
noGroupByUser,
client.userID,
],
);
+ /**
+ * @deprecated use `useMessageGroupStyles` hook instead directly in the Message.
+ */
const messageGroupStylesRef = useRef(messageGroupStyles);
messageGroupStylesRef.current = messageGroupStyles;
- const processedMessageList = useMemo(() => {
- const newMessageList = [];
- for (const message of messageList) {
- if (
- shouldIncludeMessageInList(message, {
- deletedMessagesVisibilityType,
- userId: client.userID,
- })
- ) {
- if (isFlashList) {
- newMessageList.push(message);
- } else {
- newMessageList.unshift(message);
- }
- }
- }
- return newMessageList;
- }, [client.userID, deletedMessagesVisibilityType, isFlashList, messageList]);
-
const data = useRAFCoalescedValue(processedMessageList, isLiveStreaming);
return useMemo(
@@ -125,12 +156,13 @@ export const useMessageList = (params: UseMessageListParams) => {
dateSeparatorsRef,
/** Message group styles */
messageGroupStylesRef,
+ messageListPreviousAndNextMessageStore,
/** Messages enriched with dates/readby/groups and also reversed in order */
processedMessageList: data,
/** Raw messages from the channel state */
rawMessageList: messageList,
viewabilityChangedCallback,
}),
- [data, messageList, viewabilityChangedCallback],
+ [data, messageList, messageListPreviousAndNextMessageStore, viewabilityChangedCallback],
);
};
diff --git a/package/src/components/MessageList/utils/__tests__/getDateSeparators.test.ts b/package/src/components/MessageList/utils/__tests__/getDateSeparators.test.ts
deleted file mode 100644
index c6ee0e346e..0000000000
--- a/package/src/components/MessageList/utils/__tests__/getDateSeparators.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import type { PaginatedMessageListContextValue } from '../../../../contexts/paginatedMessageListContext/PaginatedMessageListContext';
-import { getDateSeparators } from '../getDateSeparators';
-
-describe('getDateSeparators', () => {
- it('should return an empty object if no messages are passed', () => {
- expect(getDateSeparators({ messages: [] })).toEqual({});
- });
-
- it('should return a date separator for each message in a new day', () => {
- const firstDate = new Date('2020-01-01T00:00:00.000Z');
- const secondDate = new Date('2020-01-02T00:00:00.000Z');
- const messages = [
- {
- created_at: firstDate,
- id: '1',
- text: 'foo',
- },
- {
- created_at: secondDate,
- id: '2',
- text: 'bar',
- },
- {
- created_at: secondDate,
- id: '3',
- text: 'baz',
- },
- ] as PaginatedMessageListContextValue['messages'];
-
- expect(getDateSeparators({ messages })).toEqual({
- 1: firstDate,
- 2: secondDate,
- });
- });
-
- it('should return a date separator for the visible message in a day if deleted messages are not visible to the user', () => {
- const firstDate = new Date('2020-01-01T00:00:00.000Z');
- const secondDate = new Date('2020-01-02T00:00:00.000Z');
-
- const messages = [
- {
- created_at: firstDate,
- id: '1',
- text: 'foo',
- type: 'regular',
- },
- {
- created_at: secondDate,
- id: '2',
- text: 'bar',
- type: 'deleted',
- },
- {
- created_at: secondDate,
- id: '3',
- text: 'baz',
- type: 'regular',
- },
- ] as PaginatedMessageListContextValue['messages'];
-
- expect(getDateSeparators({ deletedMessagesVisibilityType: 'never', messages })).toEqual({
- 1: firstDate,
- 3: secondDate,
- });
-
- expect(getDateSeparators({ deletedMessagesVisibilityType: 'receiver', messages })).toEqual({
- 1: firstDate,
- 3: secondDate,
- });
- });
-});
diff --git a/package/src/components/MessageList/utils/getDateSeparators.ts b/package/src/components/MessageList/utils/getDateSeparators.ts
index 90f35d5c32..575debf957 100644
--- a/package/src/components/MessageList/utils/getDateSeparators.ts
+++ b/package/src/components/MessageList/utils/getDateSeparators.ts
@@ -2,10 +2,19 @@ import type { DeletedMessagesVisibilityType } from '../../../contexts/messagesCo
import type { PaginatedMessageListContextValue } from '../../../contexts/paginatedMessageListContext/PaginatedMessageListContext';
import type { ThreadContextValue } from '../../../contexts/threadContext/ThreadContext';
+/**
+ * @deprecated in favor of `useDateSeparator` hook instead directly in the Message.
+ */
export type GetDateSeparatorsParams = {
messages: PaginatedMessageListContextValue['messages'] | ThreadContextValue['threadMessages'];
+ /**
+ * @deprecated The computations are done ahead of time in the useMessageList hook so this parameter is no longer needed.
+ */
deletedMessagesVisibilityType?: DeletedMessagesVisibilityType;
hideDateSeparators?: boolean;
+ /**
+ * @deprecated The computations are done ahead of time in the useMessageList hook so this parameter is no longer needed.
+ */
userId?: string;
};
@@ -13,33 +22,20 @@ export type DateSeparators = {
[key: string]: Date;
};
+/**
+ * @deprecated in favor of `useDateSeparator` hook instead directly in the Message.
+ */
export const getDateSeparators = (params: GetDateSeparatorsParams) => {
- const { deletedMessagesVisibilityType, hideDateSeparators, messages, userId } = params;
+ const { hideDateSeparators, messages } = params;
const dateSeparators: DateSeparators = {};
if (hideDateSeparators) {
return dateSeparators;
}
- const messagesWithoutDeleted = messages.filter((message) => {
- const isMessageTypeDeleted = message.type === 'deleted';
-
- const isDeletedMessageVisibleToSender =
- deletedMessagesVisibilityType === 'sender' || deletedMessagesVisibilityType === 'always';
-
- const isDeletedMessageVisibleToReceiver =
- deletedMessagesVisibilityType === 'receiver' || deletedMessagesVisibilityType === 'always';
-
- return (
- !isMessageTypeDeleted ||
- (userId === message.user?.id && isDeletedMessageVisibleToSender) ||
- (userId !== message.user?.id && isDeletedMessageVisibleToReceiver)
- );
- });
-
- for (let i = 0; i < messagesWithoutDeleted.length; i++) {
- const previousMessage = messagesWithoutDeleted[i - 1];
- const message = messagesWithoutDeleted[i];
+ for (let i = 0; i < messages.length; i++) {
+ const previousMessage = messages[i - 1];
+ const message = messages[i];
const messageDate = message.created_at.toDateString();
diff --git a/package/src/components/MessageList/utils/getGroupStyles.ts b/package/src/components/MessageList/utils/getGroupStyles.ts
index 56de390307..55630bd6f2 100644
--- a/package/src/components/MessageList/utils/getGroupStyles.ts
+++ b/package/src/components/MessageList/utils/getGroupStyles.ts
@@ -8,25 +8,46 @@ import type { ThreadContextValue } from '../../../contexts/threadContext/ThreadC
import { isEditedMessage } from '../../../utils/utils';
import type { GroupType } from '../hooks/useMessageList';
+export type MessageGroupStylesParams = {
+ message: LocalMessage;
+ previousMessage: LocalMessage;
+ nextMessage: LocalMessage;
+ maxTimeBetweenGroupedMessages?: number;
+ dateSeparatorDate?: Date;
+ nextMessageDateSeparatorDate?: Date;
+};
+
+/**
+ * @deprecated in favor of `useMessageGroupStyles` hook instead directly in the Message.
+ */
export type GetGroupStylesParams = {
dateSeparators: DateSeparators;
messages: PaginatedMessageListContextValue['messages'] | ThreadContextValue['threadMessages'];
+ /**
+ * @deprecated in favor of `useDateSeparator` hook instead directly in the Message.
+ */
hideDateSeparators?: boolean;
maxTimeBetweenGroupedMessages?: number;
noGroupByUser?: boolean;
+ /**
+ * @deprecated
+ */
userId?: string;
};
export type GroupStyle = '' | 'middle' | 'top' | 'bottom' | 'single';
-const getGroupStyle = (
- dateSeparators: DateSeparators,
- message: LocalMessage,
- previousMessage: LocalMessage,
- nextMessage: LocalMessage,
- hideDateSeparators?: boolean,
- maxTimeBetweenGroupedMessages?: number,
-): GroupStyle[] => {
+/**
+ * Get the group styles for a message
+ */
+export const getGroupStyle = ({
+ message,
+ previousMessage,
+ nextMessage,
+ maxTimeBetweenGroupedMessages,
+ nextMessageDateSeparatorDate,
+ dateSeparatorDate,
+}: MessageGroupStylesParams): GroupStyle[] => {
const groupStyles: GroupStyle[] = [];
const isPrevMessageTypeDeleted = previousMessage?.type === 'deleted';
@@ -41,7 +62,7 @@ const getGroupStyle = (
userId !== previousMessage?.user?.id ||
!!isPrevMessageTypeDeleted ||
// NOTE: This is needed for the group styles to work after the message separated by date.
- (!hideDateSeparators && dateSeparators[message.id]) ||
+ dateSeparatorDate ||
isEditedMessage(previousMessage);
const isBottomMessage =
@@ -50,7 +71,7 @@ const getGroupStyle = (
nextMessage.type === 'error' ||
userId !== nextMessage?.user?.id ||
!!isNextMessageTypeDeleted ||
- (!hideDateSeparators && dateSeparators[nextMessage.id]) ||
+ nextMessageDateSeparatorDate ||
(maxTimeBetweenGroupedMessages !== undefined &&
(nextMessage.created_at as Date).getTime() - (message.created_at as Date).getTime() >
maxTimeBetweenGroupedMessages) ||
@@ -98,15 +119,11 @@ const getGroupStyle = (
return groupStyles;
};
+/**
+ * @deprecated in favor of `useMessageGroupStyles` hook instead directly in the Message.
+ */
export const getGroupStyles = (params: GetGroupStylesParams) => {
- const {
- dateSeparators,
- hideDateSeparators,
- maxTimeBetweenGroupedMessages,
- messages,
- noGroupByUser,
- userId,
- } = params;
+ const { dateSeparators, maxTimeBetweenGroupedMessages, messages, noGroupByUser } = params;
if (noGroupByUser) {
return {};
@@ -114,25 +131,19 @@ export const getGroupStyles = (params: GetGroupStylesParams) => {
const messageGroupStyles: { [key: string]: GroupType[] } = {};
- const messagesFilteredForNonUser = messages.filter((message) => {
- const isMessageTypeDeleted = message.type === 'deleted';
- return !isMessageTypeDeleted || userId === message.user?.id;
- });
-
- for (let i = 0; i < messagesFilteredForNonUser.length; i++) {
- const previousMessage = messagesFilteredForNonUser[i - 1];
- const message = messagesFilteredForNonUser[i];
- const nextMessage = messagesFilteredForNonUser[i + 1];
+ for (let i = 0; i < messages.length; i++) {
+ const previousMessage = messages[i - 1];
+ const message = messages[i];
+ const nextMessage = messages[i + 1];
if (message.id) {
- messageGroupStyles[message.id] = getGroupStyle(
- dateSeparators,
+ messageGroupStyles[message.id] = getGroupStyle({
+ dateSeparatorDate: dateSeparators[message.id],
+ maxTimeBetweenGroupedMessages,
message,
- previousMessage,
nextMessage,
- hideDateSeparators,
- maxTimeBetweenGroupedMessages,
- );
+ previousMessage,
+ });
}
}
diff --git a/package/src/components/MessageList/utils/getLastReceivedMessage.ts b/package/src/components/MessageList/utils/getLastReceivedMessage.ts
index 2752950258..d53e15c7d8 100644
--- a/package/src/components/MessageList/utils/getLastReceivedMessage.ts
+++ b/package/src/components/MessageList/utils/getLastReceivedMessage.ts
@@ -7,10 +7,7 @@ export const getLastReceivedMessage = (messages: LocalMessage[]) => {
* There are no status on dates so they will be skipped
*/
for (const message of messages) {
- if (
- message?.status === MessageStatusTypes.RECEIVED ||
- message?.status === MessageStatusTypes.SENDING
- ) {
+ if (message?.status !== MessageStatusTypes.FAILED) {
return message;
}
}
diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
index a445b548d4..a917135ec3 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
@@ -232,10 +232,10 @@ exports[`Thread should match thread snapshot 1`] = `
}
>
>;
+ setChannelUnreadState?: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void;
setTargetedMessage?: (messageId: string) => void;
}) => Promise;
@@ -123,7 +127,7 @@ export type ChannelContextValue = {
read: ChannelState['read'];
reloadChannel: () => Promise;
scrollToFirstUnreadThreshold: number;
- setChannelUnreadState: React.Dispatch>;
+ setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void;
setLastRead: React.Dispatch>;
setTargetedMessage: (messageId?: string) => void;
/**
@@ -131,7 +135,12 @@ export type ChannelContextValue = {
* Its a map of filename and AbortController
*/
uploadAbortControllerRef: React.MutableRefObject