Skip to content

Commit 5561c28

Browse files
isekovanicStream-SDK-BotStream Bot
authored
Next Release (#3705)
## 🎯 Goal <!-- Describe why we are making this change --> ## πŸ›  Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## πŸ§ͺ Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## β˜‘οΈ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --------- Co-authored-by: Stream SDK Bot <60655709+Stream-SDK-Bot@users.noreply.github.com> Co-authored-by: Stream Bot <runner@runnervmkkn4f.zyhczbmvobnubd5qxt4auap1gc.gx.internal.cloudapp.net>
2 parents bf63820 + 9e89a66 commit 5561c28

17 files changed

Lines changed: 147 additions & 43 deletions

File tree

β€ŽREADME.mdβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
[![NPM](https://img.shields.io/npm/v/stream-chat-react-native.svg)](https://www.npmjs.com/package/stream-chat-react-native)
1111
[![Build Status](https://github.com/GetStream/stream-chat-react-native/actions/workflows/release.yml/badge.svg)](https://github.com/GetStream/stream-chat-react-native/actions)
1212
[![Component Reference](https://img.shields.io/badge/docs-component%20reference-blue.svg)](https://getstream.io/chat/docs/sdk/reactnative)
13-
![JS Bundle Size](https://img.shields.io/badge/js_bundle_size-1974%20KB-blue)
13+
![JS Bundle Size](https://img.shields.io/badge/js_bundle_size-1975%20KB-blue)
1414

1515
<img align="right" src="https://getstream.imgix.net/images/ios-chat-tutorial/iphone_chat_art@3x.png?auto=format,enhance" width="50%" />
1616

β€Žexamples/ExpoMessaging/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"react-native-teleport": "^1.0.2",
5252
"react-native-web": "^0.21.0",
5353
"react-native-worklets": "0.8.3",
54-
"stream-chat": "^9.48.0",
54+
"stream-chat": "^9.50.0",
5555
"stream-chat-expo": "workspace:^",
5656
"stream-chat-react-native-core": "workspace:^"
5757
},

β€Žexamples/SampleApp/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"react-native-teleport": "^1.1.7",
6565
"react-native-video": "^6.19.2",
6666
"react-native-worklets": "^0.8.3",
67-
"stream-chat": "^9.48.0",
67+
"stream-chat": "^9.50.0",
6868
"stream-chat-react-native": "workspace:^",
6969
"stream-chat-react-native-core": "workspace:^"
7070
},

β€Žexamples/TypeScriptMessaging/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"react-native-svg": "^15.12.0",
3434
"react-native-video": "^6.16.1",
3535
"react-native-worklets": "^0.4.1",
36-
"stream-chat": "^9.48.0",
36+
"stream-chat": "^9.50.0",
3737
"stream-chat-react-native": "workspace:^",
3838
"stream-chat-react-native-core": "workspace:^"
3939
},

β€Žpackage/expo-package/src/index.jsβ€Ž

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
Video,
2525
} from './optionalDependencies';
2626

27-
registerNativeHandlers({
27+
/**
28+
* The default native handlers this package registers with the core SDK.
29+
*/
30+
export const defaultNativeHandlers = {
2831
Audio,
2932
compressImage,
3033
deleteFile,
@@ -39,13 +42,17 @@ registerNativeHandlers({
3942
pickDocument,
4043
pickImage,
4144
saveFile,
42-
SDK: 'stream-chat-expo',
4345
setClipboardString,
4446
shareImage,
4547
Sound,
4648
takePhoto,
4749
triggerHaptic,
4850
Video,
51+
};
52+
53+
registerNativeHandlers({
54+
...defaultNativeHandlers,
55+
SDK: 'stream-chat-expo',
4956
});
5057

5158
export * from 'stream-chat-react-native-core';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1+
import { registerNativeHandlers } from 'stream-chat-react-native-core';
2+
13
export * from 'stream-chat-react-native-core';
4+
5+
/**
6+
* The default native handlers this package registers with the core SDK.
7+
*
8+
* Exposed so integrators can compose or wrap a single handler (for example to
9+
* force `takePhoto` to capture images only) without reimplementing it or
10+
* reaching into internal module paths. Register your override *after* importing
11+
* this package so it takes precedence.
12+
*
13+
* Example:
14+
*
15+
* ```ts
16+
* import { registerNativeHandlers, defaultNativeHandlers } from 'stream-chat-expo';
17+
*
18+
* const localTakePhoto = defaultNativeHandlers.takePhoto;
19+
*
20+
* registerNativeHandlers({
21+
* takePhoto: localTakePhoto
22+
* ? (options) => {
23+
* console.log('[#3379 demo] wrapped takePhoto β€” forcing mediaType "image"', options);
24+
* return localTakePhoto({ ...options, mediaType: 'image' });
25+
* }
26+
* : undefined,
27+
* });
28+
* ```
29+
*/
30+
export declare const defaultNativeHandlers: Parameters<typeof registerNativeHandlers>[0];

β€Žpackage/native-package/src/index.jsβ€Ž

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
Video,
2626
} from './optionalDependencies';
2727

28-
registerNativeHandlers({
28+
/**
29+
* The default native handlers this package registers with the core SDK.
30+
*/
31+
export const defaultNativeHandlers = {
2932
Audio,
3033
compressImage,
3134
deleteFile,
@@ -40,13 +43,17 @@ registerNativeHandlers({
4043
pickDocument,
4144
pickImage,
4245
saveFile,
43-
SDK: 'stream-chat-react-native',
4446
setClipboardString,
4547
shareImage,
4648
Sound,
4749
takePhoto,
4850
triggerHaptic,
4951
Video,
52+
};
53+
54+
registerNativeHandlers({
55+
...defaultNativeHandlers,
56+
SDK: 'stream-chat-react-native',
5057
});
5158

5259
if (Platform.OS === 'android') {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1+
import { registerNativeHandlers } from 'stream-chat-react-native-core';
2+
13
export * from 'stream-chat-react-native-core';
4+
5+
/**
6+
* The default native handlers this package registers with the core SDK.
7+
*
8+
* Exposed so integrators can compose or wrap a single handler (for example to
9+
* force `takePhoto` to capture images only) without reimplementing it or
10+
* reaching into internal module paths. Register your override *after* importing
11+
* this package so it takes precedence.
12+
*
13+
* Example:
14+
*
15+
* ```ts
16+
* import { registerNativeHandlers, defaultNativeHandlers } from 'stream-chat-expo';
17+
*
18+
* const localTakePhoto = defaultNativeHandlers.takePhoto;
19+
*
20+
* registerNativeHandlers({
21+
* takePhoto: localTakePhoto
22+
* ? (options) => {
23+
* console.log('[#3379 demo] wrapped takePhoto β€” forcing mediaType "image"', options);
24+
* return localTakePhoto({ ...options, mediaType: 'image' });
25+
* }
26+
* : undefined,
27+
* });
28+
* ```
29+
*/
30+
export declare const defaultNativeHandlers: Parameters<typeof registerNativeHandlers>[0];

β€Žpackage/package.jsonβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"path": "0.12.7",
7979
"react-native-markdown-package": "1.8.2",
8080
"react-native-url-polyfill": "^2.0.0",
81-
"stream-chat": "^9.48.0",
81+
"stream-chat": "^9.50.0",
8282
"use-sync-external-store": "^1.5.0"
8383
},
8484
"peerDependencies": {

β€Žpackage/src/components/Channel/Channel.tsxβ€Ž

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -511,12 +511,12 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
511511
const styles = useStyles();
512512
const [deleted, setDeleted] = useState<boolean>(false);
513513
const [error, setError] = useState<Error | boolean>(false);
514-
const [lastRead, setLastRead] = useState<Date | undefined>();
514+
const lastReadRef = useRef<Date | undefined>(undefined);
515515
const [thread, setThread] = useState<LocalMessage | null>(threadProps || null);
516516
const [threadHasMore, setThreadHasMore] = useState(true);
517517
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
518-
const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
519-
const [messageInputHeightStore] = useState(new MessageInputHeightStore());
518+
const [channelUnreadStateStore] = useState(() => new ChannelUnreadStateStore());
519+
const [messageInputHeightStore] = useState(() => new MessageInputHeightStore());
520520
// TODO: Think if we can remove this and just rely on the channelUnreadStateStore everywhere.
521521
const setChannelUnreadState = useCallback(
522522
(data: ChannelUnreadStateStoreType['channelUnreadState']) => {
@@ -690,6 +690,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
690690
return;
691691
}
692692

693+
if (event.type === 'message.read_locally') {
694+
// When local unread reset happens, the count is already updated in the client state,
695+
// and the preview badge / unread divider are handled elsewhere, so there is nothing
696+
// to copy into channel state here. Thus, we skip it.
697+
return;
698+
}
699+
693700
if (event.type === 'message.read' || event.type === 'notification.mark_read') {
694701
setReadThrottled();
695702
return;
@@ -703,7 +710,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
703710
useEffect(() => {
704711
let listener: ReturnType<typeof channel.on>;
705712
const initChannel = async () => {
706-
setLastRead(new Date());
713+
lastReadRef.current = new Date();
707714
const unreadCount = channel.countUnread();
708715
const shouldLoadAtFirstUnread = shouldLoadInitialChannelAtFirstUnreadMessage(unreadCount);
709716
if (!channel || !shouldSyncChannel) {
@@ -812,7 +819,25 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
812819
const markReadInternal: ChannelContextValue['markRead'] = throttle(
813820
async (options?: MarkReadFunctionOptions) => {
814821
const { updateChannelUnreadState = true } = options ?? {};
815-
if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
822+
if (!channel || channel?.disconnected) {
823+
return;
824+
}
825+
826+
// When read events are disabled (e.g. livestreams) we cannot mark read on the backend. If the
827+
// client opted into a local unread count, reset it locally instead so the user's "caught up"
828+
// state is reflected without a server round trip.
829+
if (!clientChannelConfig?.read_events) {
830+
if (client.options.isLocalUnreadCountEnabled) {
831+
const event = channel.markReadLocally();
832+
if (updateChannelUnreadState && event && lastReadRef.current) {
833+
setChannelUnreadState({
834+
last_read: lastReadRef.current,
835+
last_read_message_id: event.last_read_message_id,
836+
unread_messages: 0,
837+
});
838+
lastReadRef.current = new Date();
839+
}
840+
}
816841
return;
817842
}
818843

@@ -821,13 +846,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
821846
} else {
822847
try {
823848
const response = await channel.markRead();
824-
if (updateChannelUnreadState && response && lastRead) {
849+
if (updateChannelUnreadState && response && lastReadRef.current) {
825850
setChannelUnreadState({
826-
last_read: lastRead,
851+
last_read: lastReadRef.current,
827852
last_read_message_id: response?.event.last_read_message_id,
828853
unread_messages: 0,
829854
});
830-
setLastRead(new Date());
855+
lastReadRef.current = new Date();
831856
}
832857
} catch (err) {
833858
console.log('Error marking channel as read:', err);
@@ -1578,7 +1603,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
15781603
hideStickyDateHeader,
15791604
highlightedMessageId,
15801605
isChannelActive: shouldSyncChannel,
1581-
lastRead,
15821606
loadChannelAroundMessage,
15831607
loadChannelAtFirstUnreadMessage,
15841608
loading: channelMessagesState.loading,
@@ -1590,7 +1614,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
15901614
reloadChannel,
15911615
scrollToFirstUnreadThreshold,
15921616
setChannelUnreadState,
1593-
setLastRead,
15941617
setTargetedMessage,
15951618
hasPendingInitialTargetLoad,
15961619
targetedMessage,

0 commit comments

Comments
Β (0)