stream-chat 9.41.0 → 9.42.0

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.
@@ -0,0 +1,176 @@
1
+ import type { StreamChat } from './client';
2
+ import type { UploadRequestOptions } from './messageComposer/configuration/types';
3
+ import { StateStore } from './store';
4
+ import type { AttachmentManager } from '.';
5
+
6
+ export type UploadRecord = {
7
+ id: string;
8
+ uploadProgress?: number;
9
+ };
10
+
11
+ export type UploadManagerState = {
12
+ uploads: Record<string, UploadRecord>;
13
+ };
14
+
15
+ const initState = (): UploadManagerState => ({ uploads: {} });
16
+
17
+ const upsertById = (
18
+ uploads: Record<string, UploadRecord>,
19
+ record: UploadRecord,
20
+ ): Record<string, UploadRecord> => ({
21
+ ...uploads,
22
+ [record.id]: { ...uploads[record.id], ...record },
23
+ });
24
+
25
+ const updateById = (
26
+ uploads: Record<string, UploadRecord>,
27
+ record: UploadRecord,
28
+ ): Record<string, UploadRecord> | null => {
29
+ if (!(record.id in uploads)) return null;
30
+ const current = uploads[record.id];
31
+ return { ...uploads, [record.id]: { ...current, ...record } };
32
+ };
33
+
34
+ type UploadPromise = ReturnType<typeof AttachmentManager.prototype.doUploadRequest>;
35
+
36
+ type InFlightUpload = { promise: UploadPromise; abortController: AbortController };
37
+
38
+ /**
39
+ * @internal
40
+ */
41
+ export class UploadManager {
42
+ readonly state: StateStore<UploadManagerState>;
43
+
44
+ private inFlightUploads = new Map<string, InFlightUpload>();
45
+
46
+ constructor(private readonly client: StreamChat) {
47
+ this.state = new StateStore<UploadManagerState>(initState());
48
+ }
49
+
50
+ private resolveAttachmentManager(channelCid: string) {
51
+ const colon = channelCid.indexOf(':');
52
+ if (colon <= 0 || colon === channelCid.length - 1) {
53
+ throw new Error(`Invalid channelCid: ${channelCid}`);
54
+ }
55
+ const channelType = channelCid.slice(0, colon);
56
+ const channelId = channelCid.slice(colon + 1);
57
+ return this.client.channel(channelType, channelId).messageComposer.attachmentManager;
58
+ }
59
+
60
+ get uploads() {
61
+ return this.state.getLatestValue().uploads;
62
+ }
63
+
64
+ getUpload = (id: string) => this.uploads[id];
65
+
66
+ /**
67
+ * Clears all upload records.
68
+ * Invoked when the user disconnects so a later session does not inherit stale upload state.
69
+ * Aborts every in-flight upload request via its `UploadRequestOptions.abortSignal`.
70
+ */
71
+ reset = () => {
72
+ for (const { abortController } of this.inFlightUploads.values()) {
73
+ abortController.abort();
74
+ }
75
+ this.inFlightUploads.clear();
76
+ this.state.next(initState());
77
+ };
78
+
79
+ /**
80
+ * Removes the upload record for `id` if present.
81
+ * If an upload is still in progress, aborts its `UploadRequestOptions.abortSignal`.
82
+ */
83
+ deleteUploadRecord = (id: string) => {
84
+ const flight = this.inFlightUploads.get(id);
85
+ if (flight) {
86
+ this.inFlightUploads.delete(id);
87
+ flight.abortController.abort();
88
+ }
89
+ this.state.next((current) => {
90
+ if (!(id in current.uploads)) return current;
91
+ const uploads = { ...current.uploads };
92
+ delete uploads[id];
93
+ return { ...current, uploads };
94
+ });
95
+ };
96
+
97
+ /**
98
+ * Starts an upload for `id`, or returns the existing in-flight promise if one is already running.
99
+ * Uses {@link StreamChat.channel}(`channelCid`) → `messageComposer.attachmentManager.doUploadRequest`.
100
+ * Resolves with that result; rejects if the upload rejects (the record is removed from state either way).
101
+ */
102
+ upload = ({
103
+ id,
104
+ channelCid,
105
+ file,
106
+ }: {
107
+ id: string;
108
+ channelCid: string;
109
+ file: Parameters<typeof AttachmentManager.prototype.doUploadRequest>[0];
110
+ }): ReturnType<typeof AttachmentManager.prototype.doUploadRequest> => {
111
+ const existing = this.inFlightUploads.get(id);
112
+ if (existing) return existing.promise;
113
+
114
+ let resolvePromise!: (value: Awaited<UploadPromise>) => void;
115
+ let rejectPromise!: (reason?: unknown) => void;
116
+ const promise = new Promise<Awaited<UploadPromise>>((resolve, reject) => {
117
+ resolvePromise = resolve;
118
+ rejectPromise = reject;
119
+ });
120
+
121
+ const abortController = new AbortController();
122
+ this.inFlightUploads.set(id, { promise, abortController });
123
+
124
+ void (async () => {
125
+ const attachmentManager = this.resolveAttachmentManager(channelCid);
126
+ const trackProgress = attachmentManager.config.trackUploadProgress;
127
+ try {
128
+ this.upsertUpload({
129
+ id,
130
+ uploadProgress: trackProgress ? 0 : undefined,
131
+ });
132
+
133
+ const onProgress = trackProgress
134
+ ? (progress?: number) => {
135
+ this.updateUpload({
136
+ id,
137
+ uploadProgress: progress,
138
+ });
139
+ }
140
+ : undefined;
141
+
142
+ const uploadRequestOptions: UploadRequestOptions = {
143
+ abortSignal: abortController.signal,
144
+ ...(onProgress ? { onProgress } : {}),
145
+ };
146
+
147
+ const response = await attachmentManager.doUploadRequest(
148
+ file,
149
+ uploadRequestOptions,
150
+ );
151
+ resolvePromise(response);
152
+ } catch (error) {
153
+ rejectPromise(error);
154
+ } finally {
155
+ this.inFlightUploads.delete(id);
156
+ this.deleteUploadRecord(id);
157
+ }
158
+ })();
159
+
160
+ return promise;
161
+ };
162
+
163
+ private upsertUpload = (record: UploadRecord) => {
164
+ this.state.partialNext({
165
+ uploads: upsertById(this.uploads, record),
166
+ });
167
+ };
168
+
169
+ private updateUpload = (record: UploadRecord) => {
170
+ this.state.next((current) => {
171
+ const nextUploads = updateById(current.uploads, record);
172
+ if (!nextUploads) return current;
173
+ return { ...current, uploads: nextUploads };
174
+ });
175
+ };
176
+ }
package/src/utils.ts CHANGED
@@ -811,6 +811,7 @@ type MessagePaginationUpdatedParams = {
811
811
  parentSet: MessageSet;
812
812
  requestedPageSize: number;
813
813
  returnedPage: MessageResponse[];
814
+ filteredReturnedPage: MessageResponse[];
814
815
  logger?: Logger;
815
816
  messagePaginationOptions?: MessagePaginationOptions;
816
817
  };
@@ -849,6 +850,7 @@ const messagePaginationCreatedAtAround = ({
849
850
  parentSet,
850
851
  requestedPageSize,
851
852
  returnedPage,
853
+ filteredReturnedPage,
852
854
  messagePaginationOptions,
853
855
  }: MessagePaginationUpdatedParams) => {
854
856
  const newPagination = { ...parentSet.pagination };
@@ -892,9 +894,14 @@ const messagePaginationCreatedAtAround = ({
892
894
  hasNext = hasPrev = false;
893
895
  updateHasPrev = updateHasNext = true;
894
896
  } else {
897
+ const [firstFilteredPageMsg, lastFilteredPageMsg] = [
898
+ filteredReturnedPage[0],
899
+ filteredReturnedPage.slice(-1)[0],
900
+ ];
895
901
  const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
896
- firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id,
897
- lastPageMsg?.id && lastPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
902
+ firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id,
903
+ lastFilteredPageMsg?.id &&
904
+ lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
898
905
  ];
899
906
  updateHasPrev = firstPageMsgIsFirstInSet;
900
907
  updateHasNext = lastPageMsgIsLastInSet;
@@ -920,6 +927,7 @@ const messagePaginationIdAround = ({
920
927
  parentSet,
921
928
  requestedPageSize,
922
929
  returnedPage,
930
+ filteredReturnedPage,
923
931
  messagePaginationOptions,
924
932
  }: MessagePaginationUpdatedParams) => {
925
933
  const newPagination = { ...parentSet.pagination };
@@ -928,10 +936,13 @@ const messagePaginationIdAround = ({
928
936
  let hasPrev;
929
937
  let hasNext;
930
938
 
931
- const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
939
+ const [firstFilteredPageMsg, lastFilteredPageMsg] = [
940
+ filteredReturnedPage[0],
941
+ filteredReturnedPage.slice(-1)[0],
942
+ ];
932
943
  const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
933
- firstPageMsg?.id === parentSet.messages[0]?.id,
934
- lastPageMsg?.id === parentSet.messages.slice(-1)[0]?.id,
944
+ firstFilteredPageMsg?.id === parentSet.messages[0]?.id,
945
+ lastFilteredPageMsg?.id === parentSet.messages.slice(-1)[0]?.id,
935
946
  ];
936
947
  let updateHasPrev = firstPageMsgIsFirstInSet;
937
948
  let updateHasNext = lastPageMsgIsLastInSet;
@@ -974,6 +985,7 @@ const messagePaginationLinear = ({
974
985
  parentSet,
975
986
  requestedPageSize,
976
987
  returnedPage,
988
+ filteredReturnedPage,
977
989
  messagePaginationOptions,
978
990
  }: MessagePaginationUpdatedParams) => {
979
991
  const newPagination = { ...parentSet.pagination };
@@ -981,10 +993,14 @@ const messagePaginationLinear = ({
981
993
  let hasPrev;
982
994
  let hasNext;
983
995
 
984
- const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
996
+ const [firstFilteredPageMsg, lastFilteredPageMsg] = [
997
+ filteredReturnedPage[0],
998
+ filteredReturnedPage.slice(-1)[0],
999
+ ];
985
1000
  const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
986
- firstPageMsg?.id && firstPageMsg.id === parentSet.messages[0]?.id,
987
- lastPageMsg?.id && lastPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
1001
+ firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id,
1002
+ lastFilteredPageMsg?.id &&
1003
+ lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
988
1004
  ];
989
1005
 
990
1006
  const queriedNextMessages =
@@ -1028,9 +1044,9 @@ const messagePaginationLinear = ({
1028
1044
  };
1029
1045
 
1030
1046
  export const messageSetPagination = (params: MessagePaginationUpdatedParams) => {
1031
- const messagesFilteredLocally = params.returnedPage.filter(({ shadowed }) => shadowed);
1032
1047
  if (
1033
- params.parentSet.messages.length + messagesFilteredLocally.length <
1048
+ params.parentSet.messages.length +
1049
+ (params.returnedPage.length - params.filteredReturnedPage.length) <
1034
1050
  params.returnedPage.length
1035
1051
  ) {
1036
1052
  params.logger?.(