@amityco/ts-sdk 7.1.0 → 7.1.1-17d9051c.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.
- package/.env +26 -26
- package/dist/@types/core/events.d.ts +2 -1
- package/dist/@types/core/events.d.ts.map +1 -1
- package/dist/@types/core/model.d.ts +2 -0
- package/dist/@types/core/model.d.ts.map +1 -1
- package/dist/@types/core/readReceipt.d.ts +12 -1
- package/dist/@types/core/readReceipt.d.ts.map +1 -1
- package/dist/@types/domains/channel.d.ts +10 -0
- package/dist/@types/domains/channel.d.ts.map +1 -1
- package/dist/@types/domains/client.d.ts +1 -0
- package/dist/@types/domains/client.d.ts.map +1 -1
- package/dist/channelRepository/api/markChannelsAsReadBySegment.d.ts +16 -0
- package/dist/channelRepository/api/markChannelsAsReadBySegment.d.ts.map +1 -0
- package/dist/channelRepository/events/onChannelDeleted.d.ts.map +1 -1
- package/dist/channelRepository/events/onChannelLeft.d.ts.map +1 -1
- package/dist/{marker → channelRepository}/events/onChannelUnreadUpdatedLocal.d.ts +2 -2
- package/dist/channelRepository/events/onChannelUnreadUpdatedLocal.d.ts.map +1 -0
- package/dist/channelRepository/internalApi/getTotalChannelsUnread.d.ts +16 -0
- package/dist/channelRepository/internalApi/getTotalChannelsUnread.d.ts.map +1 -0
- package/dist/channelRepository/observers/getChannel.d.ts.map +1 -1
- package/dist/channelRepository/observers/getChannels/ChannelLiveCollectionController.d.ts.map +1 -1
- package/dist/channelRepository/observers/getTotalChannelsUnread.d.ts +20 -0
- package/dist/channelRepository/observers/getTotalChannelsUnread.d.ts.map +1 -0
- package/dist/channelRepository/observers/index.d.ts +1 -0
- package/dist/channelRepository/observers/index.d.ts.map +1 -1
- package/dist/channelRepository/utils/constructChannelDynamicValue.d.ts.map +1 -1
- package/dist/channelRepository/utils/getLegacyChannelUnread.d.ts +2 -0
- package/dist/channelRepository/utils/getLegacyChannelUnread.d.ts.map +1 -0
- package/dist/channelRepository/utils/prepareChannelPayload.d.ts.map +1 -1
- package/dist/client/api/createClient.d.ts.map +1 -1
- package/dist/client/api/enableUnreadCount.d.ts.map +1 -1
- package/dist/client/api/login.d.ts.map +1 -1
- package/dist/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngine.d.ts +33 -0
- package/dist/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngine.d.ts.map +1 -0
- package/dist/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngineOnLoginHandler.d.ts +3 -0
- package/dist/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngineOnLoginHandler.d.ts.map +1 -0
- package/dist/client/utils/ReadReceiptSync/readReceiptSyncEngine.d.ts +2 -4
- package/dist/client/utils/ReadReceiptSync/readReceiptSyncEngine.d.ts.map +1 -1
- package/dist/core/events.d.ts +3 -3
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/model/idResolvers.d.ts.map +1 -1
- package/dist/index.cjs.js +527 -50
- package/dist/index.esm.js +527 -50
- package/dist/index.umd.js +4 -4
- package/dist/marker/events/onChannelUnreadInfoUpdatedLocal.d.ts +12 -0
- package/dist/marker/events/onChannelUnreadInfoUpdatedLocal.d.ts.map +1 -0
- package/dist/messageRepository/events/onMessageCreated.d.ts.map +1 -1
- package/dist/messageRepository/observers/getMessage.d.ts.map +1 -1
- package/dist/messageRepository/utils/markReadMessage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/@types/core/events.ts +2 -1
- package/src/@types/core/model.ts +4 -0
- package/src/@types/core/readReceipt.ts +14 -1
- package/src/@types/domains/channel.ts +13 -0
- package/src/@types/domains/client.ts +2 -0
- package/src/channelRepository/api/markChannelsAsReadBySegment.ts +29 -0
- package/src/channelRepository/events/onChannelDeleted.ts +17 -4
- package/src/channelRepository/events/onChannelLeft.ts +11 -3
- package/src/{marker → channelRepository}/events/onChannelUnreadUpdatedLocal.ts +3 -3
- package/src/channelRepository/internalApi/getTotalChannelsUnread.ts +43 -0
- package/src/channelRepository/observers/getChannel.ts +3 -1
- package/src/channelRepository/observers/getChannels/ChannelLiveCollectionController.ts +6 -1
- package/src/channelRepository/observers/getTotalChannelsUnread.ts +130 -0
- package/src/channelRepository/observers/index.ts +1 -0
- package/src/channelRepository/utils/constructChannelDynamicValue.ts +12 -2
- package/src/channelRepository/utils/getLegacyChannelUnread.ts +5 -0
- package/src/channelRepository/utils/prepareChannelPayload.ts +57 -17
- package/src/client/api/createClient.ts +3 -0
- package/src/client/api/enableUnreadCount.ts +1 -0
- package/src/client/api/login.ts +5 -1
- package/src/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngine.ts +267 -0
- package/src/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngineOnLoginHandler.ts +21 -0
- package/src/client/utils/ReadReceiptSync/readReceiptSyncEngine.ts +70 -99
- package/src/core/model/idResolvers.ts +2 -0
- package/src/marker/events/onChannelUnreadInfoUpdatedLocal.ts +29 -0
- package/src/messageRepository/events/onMessageCreated.ts +34 -0
- package/src/messageRepository/observers/getMessage.ts +0 -1
- package/src/messageRepository/utils/markReadMessage.ts +10 -3
- package/dist/marker/events/onChannelUnreadUpdatedLocal.d.ts.map +0 -1
package/src/client/api/login.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { onUserDeleted } from '~/userRepository/events/onUserDeleted';
|
|
|
11
11
|
|
|
12
12
|
import analyticsEngineOnLoginHandler from '~/analytic/utils/analyticsEngineOnLoginHandler';
|
|
13
13
|
import readReceiptSyncEngineOnLoginHandler from '~/client/utils/ReadReceiptSync/readReceiptSyncEngineOnLoginHandler';
|
|
14
|
+
import legacyReadReceiptSyncEngineOnLoginHandler from '~/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngineOnLoginHandler';
|
|
14
15
|
import objectResolverEngineOnLoginHandler from '~/client/utils/ObjectResolver/objectResolverEngineOnLoginHandler';
|
|
15
16
|
import { logout } from './logout';
|
|
16
17
|
|
|
@@ -200,10 +201,13 @@ export const login = async (
|
|
|
200
201
|
|
|
201
202
|
markReadEngineOnLoginHandler(),
|
|
202
203
|
analyticsEngineOnLoginHandler(),
|
|
203
|
-
readReceiptSyncEngineOnLoginHandler(),
|
|
204
204
|
objectResolverEngineOnLoginHandler(),
|
|
205
205
|
);
|
|
206
206
|
|
|
207
|
+
if (client.useLegacyUnreadCount) {
|
|
208
|
+
subscriptions.push(readReceiptSyncEngineOnLoginHandler());
|
|
209
|
+
} else subscriptions.push(legacyReadReceiptSyncEngineOnLoginHandler());
|
|
210
|
+
|
|
207
211
|
const markerSyncUnsubscriber = await startMarkerSync();
|
|
208
212
|
subscriptions.push(markerSyncUnsubscriber);
|
|
209
213
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { pullFromCache, pushToCache, queryCache } from '~/cache/api';
|
|
2
|
+
import { getActiveClient } from '../../api/activeClient';
|
|
3
|
+
import { markAsReadBySegment } from '~/subChannelRepository/api/markAsReadBySegment';
|
|
4
|
+
import { reCalculateChannelUnreadInfo } from '~/marker/utils/reCalculateChannelUnreadInfo';
|
|
5
|
+
import { fireEvent } from '~/core/events';
|
|
6
|
+
|
|
7
|
+
export class LegacyMessageReadReceiptSyncEngine {
|
|
8
|
+
private client: Amity.Client;
|
|
9
|
+
|
|
10
|
+
private isActive = true;
|
|
11
|
+
|
|
12
|
+
private MAX_RETRY = 3;
|
|
13
|
+
|
|
14
|
+
private JOB_QUEUE_SIZE = 120;
|
|
15
|
+
|
|
16
|
+
private jobQueue: Amity.LegacyReadReceiptSyncJob[] = [];
|
|
17
|
+
|
|
18
|
+
private timer: NodeJS.Timer | undefined;
|
|
19
|
+
|
|
20
|
+
// Interval for message read receipt sync in seconds
|
|
21
|
+
private RECEIPT_SYNC_INTERVAL = 1;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.client = getActiveClient();
|
|
25
|
+
// Get remaining unsync read receipts from cache
|
|
26
|
+
this.getUnsyncJobs();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Call this when client call client.login
|
|
30
|
+
startSyncReadReceipt() {
|
|
31
|
+
// Start timer when start receipt sync
|
|
32
|
+
this.timer = setInterval(() => {
|
|
33
|
+
this.syncReadReceipts();
|
|
34
|
+
}, this.RECEIPT_SYNC_INTERVAL * 1000);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Read receipt observer handling
|
|
38
|
+
syncReadReceipts(): void {
|
|
39
|
+
if (this.jobQueue.length === 0 || this.isActive === false) return;
|
|
40
|
+
|
|
41
|
+
const readReceipt = this.getReadReceipt();
|
|
42
|
+
if (readReceipt) {
|
|
43
|
+
this.markReadApi(readReceipt);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private getUnsyncJobs(): void {
|
|
48
|
+
// Get all read receipts that has latestSyncSegment < latestSegment
|
|
49
|
+
const readReceipts = queryCache<Amity.LegacyReadReceipt>(['legacyReadReceipt'])?.filter(
|
|
50
|
+
({ data }) => {
|
|
51
|
+
return data.latestSyncSegment < data.latestSegment;
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Enqueue unsync read receipts to the job queue
|
|
56
|
+
readReceipts?.forEach(({ data: readReceipt }) => {
|
|
57
|
+
this.enqueueReadReceipt(readReceipt.subChannelId, readReceipt.latestSegment);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getReadReceipt(): Amity.LegacyReadReceiptSyncJob | undefined {
|
|
62
|
+
// Get first read receipt in queue
|
|
63
|
+
const syncJob = this.jobQueue[0];
|
|
64
|
+
|
|
65
|
+
if (!syncJob) return;
|
|
66
|
+
// Skip when it's syncing
|
|
67
|
+
if (syncJob.syncState === Amity.ReadReceiptSyncState.SYNCING) return;
|
|
68
|
+
|
|
69
|
+
// Get readReceipt from cache by subChannelId
|
|
70
|
+
const readReceipt = pullFromCache<Amity.LegacyReadReceipt>([
|
|
71
|
+
'legacyReadReceipt',
|
|
72
|
+
syncJob.subChannelId,
|
|
73
|
+
])?.data;
|
|
74
|
+
|
|
75
|
+
if (!readReceipt) return;
|
|
76
|
+
|
|
77
|
+
if (readReceipt?.latestSegment > readReceipt?.latestSyncSegment) {
|
|
78
|
+
syncJob.segment = readReceipt.latestSegment;
|
|
79
|
+
return syncJob;
|
|
80
|
+
}
|
|
81
|
+
// Clear all synced job in job queue
|
|
82
|
+
this.removeSynedReceipt(readReceipt.subChannelId, readReceipt.latestSegment);
|
|
83
|
+
|
|
84
|
+
// Recursion getReadReceipt() until get unsync read receipt or job queue is empty
|
|
85
|
+
return this.getReadReceipt();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async markReadApi(syncJob: Amity.LegacyReadReceiptSyncJob): Promise<void> {
|
|
89
|
+
const newSyncJob = syncJob;
|
|
90
|
+
newSyncJob.syncState = Amity.ReadReceiptSyncState.SYNCING;
|
|
91
|
+
|
|
92
|
+
const { subChannelId, segment } = newSyncJob;
|
|
93
|
+
|
|
94
|
+
const response = await markAsReadBySegment({ subChannelId, readToSegment: segment });
|
|
95
|
+
|
|
96
|
+
if (response) {
|
|
97
|
+
this.removeSynedReceipt(syncJob.subChannelId, syncJob.segment);
|
|
98
|
+
|
|
99
|
+
const readReceiptCache = pullFromCache<Amity.LegacyReadReceipt>([
|
|
100
|
+
'legacyReadReceipt',
|
|
101
|
+
subChannelId,
|
|
102
|
+
])?.data;
|
|
103
|
+
|
|
104
|
+
pushToCache(['legacyReadReceipt', subChannelId], {
|
|
105
|
+
...readReceiptCache,
|
|
106
|
+
latestSyncSegment: segment,
|
|
107
|
+
});
|
|
108
|
+
} else if (!response) {
|
|
109
|
+
if (newSyncJob.retryCount > this.MAX_RETRY) {
|
|
110
|
+
this.removeJobFromQueue(newSyncJob);
|
|
111
|
+
} else {
|
|
112
|
+
newSyncJob.retryCount += 1;
|
|
113
|
+
newSyncJob.syncState = Amity.ReadReceiptSyncState.CREATED;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private removeSynedReceipt(subChannelId: string, segment: number) {
|
|
119
|
+
const syncJobs = this.jobQueue;
|
|
120
|
+
|
|
121
|
+
syncJobs.forEach(job => {
|
|
122
|
+
if (job.subChannelId === subChannelId && job.segment <= segment) {
|
|
123
|
+
this.removeJobFromQueue(job);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private startObservingReadReceiptQueue(): void {
|
|
129
|
+
if (this.client.isUnreadCountEnabled) {
|
|
130
|
+
this.isActive = true;
|
|
131
|
+
this.startSyncReadReceipt();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private stopObservingReadReceiptQueue(): void {
|
|
136
|
+
this.isActive = false;
|
|
137
|
+
|
|
138
|
+
const syncJobs = this.jobQueue;
|
|
139
|
+
syncJobs.map(job => {
|
|
140
|
+
if (job.syncState === Amity.ReadReceiptSyncState.SYNCING) {
|
|
141
|
+
return { ...job, syncState: Amity.ReadReceiptSyncState.CREATED };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return job;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (this.timer) clearInterval(this.timer);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Session Management
|
|
151
|
+
onSessionEstablished(): void {
|
|
152
|
+
this.startObservingReadReceiptQueue();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
onSessionDestroyed(): void {
|
|
156
|
+
this.stopObservingReadReceiptQueue();
|
|
157
|
+
this.jobQueue = [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onTokenExpired(): void {
|
|
161
|
+
this.stopObservingReadReceiptQueue();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Network Connection Management
|
|
165
|
+
onNetworkOffline(): void {
|
|
166
|
+
// Stop observing to the read receipt queue.
|
|
167
|
+
this.stopObservingReadReceiptQueue();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onNetworkOnline(): void {
|
|
171
|
+
// Resume observing to the read receipt queue.
|
|
172
|
+
this.startObservingReadReceiptQueue();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
markRead(subChannelId: string, segment: number): void {
|
|
176
|
+
// Step 1: Optimistic update of subChannelUnreadInfo.readToSegment to message.segment
|
|
177
|
+
const cacheKey = ['subChannelUnreadInfo', 'get', subChannelId];
|
|
178
|
+
const subChannelUnreadInfo = pullFromCache<Amity.SubChannelUnreadInfo>(cacheKey)?.data;
|
|
179
|
+
|
|
180
|
+
if (subChannelUnreadInfo && segment > subChannelUnreadInfo.readToSegment) {
|
|
181
|
+
subChannelUnreadInfo.readToSegment = segment;
|
|
182
|
+
subChannelUnreadInfo.unreadCount = Math.max(subChannelUnreadInfo.lastSegment - segment, 0);
|
|
183
|
+
|
|
184
|
+
const channelUnreadInfo = reCalculateChannelUnreadInfo(subChannelUnreadInfo.channelId);
|
|
185
|
+
fireEvent('local.channelUnreadInfo.updated', channelUnreadInfo);
|
|
186
|
+
|
|
187
|
+
pushToCache(cacheKey, subChannelUnreadInfo);
|
|
188
|
+
fireEvent('local.subChannelUnread.updated', subChannelUnreadInfo);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 2: Enqueue the read receipt
|
|
192
|
+
this.enqueueReadReceipt(subChannelId, segment);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private enqueueReadReceipt(subChannelId: string, segment: number): void {
|
|
196
|
+
const readReceipt = pullFromCache<Amity.LegacyReadReceipt>([
|
|
197
|
+
'legacyReadReceipt',
|
|
198
|
+
subChannelId,
|
|
199
|
+
])?.data;
|
|
200
|
+
|
|
201
|
+
// Create new read receipt if it's not exists and add job to queue
|
|
202
|
+
if (!readReceipt) {
|
|
203
|
+
const readReceiptSubChannel: Amity.LegacyReadReceipt = {
|
|
204
|
+
subChannelId,
|
|
205
|
+
latestSegment: segment,
|
|
206
|
+
latestSyncSegment: 0,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
pushToCache(['legacyReadReceipt', subChannelId], readReceiptSubChannel);
|
|
210
|
+
} else if (readReceipt.latestSegment < segment) {
|
|
211
|
+
pushToCache(['legacyReadReceipt', subChannelId], { ...readReceipt, latestSegment: segment });
|
|
212
|
+
} else if (readReceipt.latestSyncSegment >= segment) {
|
|
213
|
+
// Skip the job when lastSyncSegment > = segment
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let syncJob: Amity.LegacyReadReceiptSyncJob | null = this.getSyncJob(subChannelId);
|
|
218
|
+
|
|
219
|
+
if (syncJob === null || syncJob.syncState === Amity.ReadReceiptSyncState.SYNCING) {
|
|
220
|
+
syncJob = {
|
|
221
|
+
subChannelId,
|
|
222
|
+
segment,
|
|
223
|
+
syncState: Amity.ReadReceiptSyncState.CREATED,
|
|
224
|
+
retryCount: 0,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
this.enqueueJob(syncJob);
|
|
228
|
+
} else if (syncJob.segment < segment) {
|
|
229
|
+
syncJob.segment = segment;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private getSyncJob(subChannelId: string): Amity.LegacyReadReceiptSyncJob | null {
|
|
234
|
+
const syncJobs = this.jobQueue;
|
|
235
|
+
|
|
236
|
+
const targetJob = syncJobs.find(job => job.subChannelId === subChannelId);
|
|
237
|
+
|
|
238
|
+
return targetJob || null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private enqueueJob(syncJob: Amity.LegacyReadReceiptSyncJob) {
|
|
242
|
+
if (this.jobQueue.length < this.JOB_QUEUE_SIZE) {
|
|
243
|
+
this.jobQueue.push(syncJob);
|
|
244
|
+
} else {
|
|
245
|
+
// Remove oldest job when queue reach maximum capacity
|
|
246
|
+
this.jobQueue.shift();
|
|
247
|
+
this.jobQueue.push(syncJob);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private removeJobFromQueue(item: Amity.LegacyReadReceiptSyncJob) {
|
|
252
|
+
const index = this.jobQueue.indexOf(item);
|
|
253
|
+
if (index > -1) {
|
|
254
|
+
this.jobQueue.splice(index, 1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let instance: LegacyMessageReadReceiptSyncEngine | null = null;
|
|
260
|
+
|
|
261
|
+
export default {
|
|
262
|
+
getInstance: () => {
|
|
263
|
+
if (!instance) instance = new LegacyMessageReadReceiptSyncEngine();
|
|
264
|
+
|
|
265
|
+
return instance;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { onSessionStateChange } from '~/client/events/onSessionStateChange';
|
|
2
|
+
import LegacyReadReceiptSyncEngine from '~/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngine';
|
|
3
|
+
|
|
4
|
+
export default () => {
|
|
5
|
+
const readReceiptSyncEngine = LegacyReadReceiptSyncEngine.getInstance();
|
|
6
|
+
readReceiptSyncEngine.startSyncReadReceipt();
|
|
7
|
+
|
|
8
|
+
onSessionStateChange(state => {
|
|
9
|
+
if (state === Amity.SessionStates.ESTABLISHED) {
|
|
10
|
+
readReceiptSyncEngine.onSessionEstablished();
|
|
11
|
+
} else if (state === Amity.SessionStates.TOKEN_EXPIRED) {
|
|
12
|
+
readReceiptSyncEngine.onTokenExpired();
|
|
13
|
+
} else {
|
|
14
|
+
readReceiptSyncEngine.onSessionDestroyed();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
readReceiptSyncEngine.onSessionDestroyed();
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { pullFromCache, pushToCache, queryCache } from '~/cache/api';
|
|
2
2
|
import { getActiveClient } from '../../api/activeClient';
|
|
3
|
-
import { markAsReadBySegment } from '~/subChannelRepository/api/markAsReadBySegment';
|
|
4
|
-
import { reCalculateChannelUnreadInfo } from '~/marker/utils/reCalculateChannelUnreadInfo';
|
|
5
3
|
import { fireEvent } from '~/core/events';
|
|
4
|
+
import { markChannelsAsReadBySegment } from '~/channelRepository/api/markChannelsAsReadBySegment';
|
|
6
5
|
|
|
7
6
|
export class MessageReadReceiptSyncEngine {
|
|
8
7
|
private client: Amity.Client;
|
|
@@ -38,9 +37,9 @@ export class MessageReadReceiptSyncEngine {
|
|
|
38
37
|
syncReadReceipts(): void {
|
|
39
38
|
if (this.jobQueue.length === 0 || this.isActive === false) return;
|
|
40
39
|
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
this.markReadApi(
|
|
40
|
+
const readReceipts = this.getReadReceipts();
|
|
41
|
+
if (readReceipts) {
|
|
42
|
+
this.markReadApi(readReceipts);
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
@@ -52,79 +51,64 @@ export class MessageReadReceiptSyncEngine {
|
|
|
52
51
|
|
|
53
52
|
// Enqueue unsync read receipts to the job queue
|
|
54
53
|
readReceipts?.forEach(({ data: readReceipt }) => {
|
|
55
|
-
this.enqueueReadReceipt(readReceipt.
|
|
54
|
+
this.enqueueReadReceipt(readReceipt.channelId, readReceipt.latestSegment);
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
private
|
|
60
|
-
//
|
|
61
|
-
const syncJob = this.jobQueue
|
|
58
|
+
private getReadReceipts(): Amity.ReadReceiptSyncJob[] | undefined {
|
|
59
|
+
// get all read receipts from queue, now the queue is empty
|
|
60
|
+
const syncJob = this.jobQueue.splice(0, this.jobQueue.length);
|
|
61
|
+
if (syncJob.length === 0) return;
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'readReceipt',
|
|
70
|
-
syncJob.subChannelId,
|
|
71
|
-
])?.data;
|
|
72
|
-
|
|
73
|
-
if (!readReceipt) return;
|
|
74
|
-
|
|
75
|
-
if (readReceipt?.latestSegment > readReceipt?.latestSyncSegment) {
|
|
76
|
-
syncJob.segment = readReceipt.latestSegment;
|
|
77
|
-
return syncJob;
|
|
78
|
-
}
|
|
79
|
-
// Clear all synced job in job queue
|
|
80
|
-
this.removeSynedReceipt(readReceipt.subChannelId, readReceipt.latestSegment);
|
|
81
|
-
|
|
82
|
-
// Recursion getReadReceipt() until get unsync read receipt or job queue is empty
|
|
83
|
-
return this.getReadReceipt();
|
|
63
|
+
return syncJob.filter(job => {
|
|
64
|
+
const readReceipt = pullFromCache<Amity.ReadReceipt>(['readReceipt', job.channelId])?.data;
|
|
65
|
+
if (!readReceipt) return false;
|
|
66
|
+
if (readReceipt.latestSegment > readReceipt.latestSyncSegment) return true;
|
|
67
|
+
return false;
|
|
68
|
+
});
|
|
84
69
|
}
|
|
85
70
|
|
|
86
|
-
private async markReadApi(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
private async markReadApi(syncJobs: Amity.ReadReceiptSyncJob[]): Promise<void> {
|
|
72
|
+
// constuct payload
|
|
73
|
+
// example: [{ channelId: 'channelId', readToSegment: 2 }]
|
|
74
|
+
const syncJobsPayload = syncJobs.map(job => {
|
|
75
|
+
return {
|
|
76
|
+
channelId: job.channelId,
|
|
77
|
+
readToSegment: job.segment,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
91
80
|
|
|
92
|
-
const response = await
|
|
81
|
+
const response = await markChannelsAsReadBySegment(syncJobsPayload);
|
|
93
82
|
|
|
94
83
|
if (response) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
latestSyncSegment: segment,
|
|
105
|
-
});
|
|
106
|
-
} else if (!response) {
|
|
107
|
-
if (newSyncJob.retryCount > this.MAX_RETRY) {
|
|
108
|
-
this.removeJobFromQueue(newSyncJob);
|
|
109
|
-
} else {
|
|
110
|
-
newSyncJob.retryCount += 1;
|
|
111
|
-
newSyncJob.syncState = Amity.ReadReceiptSyncState.CREATED;
|
|
84
|
+
for (let i = 0; i < syncJobs.length; i += 1) {
|
|
85
|
+
// update lastestSyncSegment in read receipt cache
|
|
86
|
+
const cacheKey = ['readReceipt', syncJobs[i].channelId];
|
|
87
|
+
const readReceiptCache = pullFromCache<Amity.ReadReceipt>(cacheKey)?.data;
|
|
88
|
+
|
|
89
|
+
pushToCache(cacheKey, {
|
|
90
|
+
...readReceiptCache,
|
|
91
|
+
latestSyncSegment: syncJobs[i].segment,
|
|
92
|
+
});
|
|
112
93
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
94
|
+
} else {
|
|
95
|
+
for (let i = 0; i < syncJobs.length; i += 1) {
|
|
96
|
+
// push them back to queue if the syncing is failed and retry count is less than max retry
|
|
97
|
+
if (syncJobs[i].retryCount >= this.MAX_RETRY) return;
|
|
115
98
|
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
const updatedJob = {
|
|
100
|
+
...syncJobs[i],
|
|
101
|
+
syncState: Amity.ReadReceiptSyncState.CREATED,
|
|
102
|
+
retryCount: syncJobs[i].retryCount + 1,
|
|
103
|
+
};
|
|
118
104
|
|
|
119
|
-
|
|
120
|
-
if (job.subChannelId === subChannelId && job.segment <= segment) {
|
|
121
|
-
this.removeJobFromQueue(job);
|
|
105
|
+
this.enqueueJob(updatedJob);
|
|
122
106
|
}
|
|
123
|
-
}
|
|
107
|
+
}
|
|
124
108
|
}
|
|
125
109
|
|
|
126
110
|
private startObservingReadReceiptQueue(): void {
|
|
127
|
-
if (this.client.
|
|
111
|
+
if (this.client.useLegacyUnreadCount) {
|
|
128
112
|
this.isActive = true;
|
|
129
113
|
this.startSyncReadReceipt();
|
|
130
114
|
}
|
|
@@ -133,8 +117,7 @@ export class MessageReadReceiptSyncEngine {
|
|
|
133
117
|
private stopObservingReadReceiptQueue(): void {
|
|
134
118
|
this.isActive = false;
|
|
135
119
|
|
|
136
|
-
|
|
137
|
-
syncJobs.map(job => {
|
|
120
|
+
this.jobQueue.map(job => {
|
|
138
121
|
if (job.syncState === Amity.ReadReceiptSyncState.SYNCING) {
|
|
139
122
|
return { ...job, syncState: Amity.ReadReceiptSyncState.CREATED };
|
|
140
123
|
}
|
|
@@ -170,50 +153,47 @@ export class MessageReadReceiptSyncEngine {
|
|
|
170
153
|
this.startObservingReadReceiptQueue();
|
|
171
154
|
}
|
|
172
155
|
|
|
173
|
-
markRead(
|
|
174
|
-
// Step 1: Optimistic update of
|
|
175
|
-
const cacheKey = ['
|
|
176
|
-
const
|
|
156
|
+
markRead(channelId: string, segment: number): void {
|
|
157
|
+
// Step 1: Optimistic update of channelUnread.readToSegment to message.segment and update unreadCount value
|
|
158
|
+
const cacheKey = ['channelUnread', 'get', channelId];
|
|
159
|
+
const channelUnread = pullFromCache<Amity.ChannelUnread>(cacheKey)?.data;
|
|
177
160
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
161
|
+
if (channelUnread && segment > channelUnread.readToSegment) {
|
|
162
|
+
channelUnread.readToSegment = segment;
|
|
163
|
+
channelUnread.unreadCount = Math.max(channelUnread.lastSegment - segment, 0);
|
|
181
164
|
|
|
182
|
-
|
|
183
|
-
fireEvent('local.channelUnread.updated',
|
|
184
|
-
|
|
185
|
-
pushToCache(cacheKey, subChannelUnreadInfo);
|
|
186
|
-
fireEvent('local.subChannelUnread.updated', subChannelUnreadInfo);
|
|
165
|
+
pushToCache(cacheKey, channelUnread);
|
|
166
|
+
fireEvent('local.channelUnread.updated', channelUnread);
|
|
187
167
|
}
|
|
188
168
|
|
|
189
169
|
// Step 2: Enqueue the read receipt
|
|
190
|
-
this.enqueueReadReceipt(
|
|
170
|
+
this.enqueueReadReceipt(channelId, segment);
|
|
191
171
|
}
|
|
192
172
|
|
|
193
|
-
private enqueueReadReceipt(
|
|
194
|
-
const readReceipt = pullFromCache<Amity.ReadReceipt>(['readReceipt',
|
|
173
|
+
private enqueueReadReceipt(channelId: string, segment: number): void {
|
|
174
|
+
const readReceipt = pullFromCache<Amity.ReadReceipt>(['readReceipt', channelId])?.data;
|
|
195
175
|
|
|
196
|
-
// Create new read receipt if it's not exists and add job to queue
|
|
176
|
+
// Create new read receipt if it's not exists and add the job to queue
|
|
197
177
|
if (!readReceipt) {
|
|
198
|
-
const
|
|
199
|
-
|
|
178
|
+
const readReceiptChannel: Amity.ReadReceipt = {
|
|
179
|
+
channelId,
|
|
200
180
|
latestSegment: segment,
|
|
201
181
|
latestSyncSegment: 0,
|
|
202
182
|
};
|
|
203
|
-
|
|
204
|
-
pushToCache(['readReceipt', subChannelId], readReceiptSubChannel);
|
|
183
|
+
pushToCache(['readReceipt', channelId], readReceiptChannel);
|
|
205
184
|
} else if (readReceipt.latestSegment < segment) {
|
|
206
|
-
|
|
185
|
+
// Update latestSegment in read receipt cache
|
|
186
|
+
pushToCache(['readReceipt', channelId], { ...readReceipt, latestSegment: segment });
|
|
207
187
|
} else if (readReceipt.latestSyncSegment >= segment) {
|
|
208
188
|
// Skip the job when lastSyncSegment > = segment
|
|
209
189
|
return;
|
|
210
190
|
}
|
|
211
191
|
|
|
212
|
-
let syncJob: Amity.ReadReceiptSyncJob | null = this.getSyncJob(
|
|
192
|
+
let syncJob: Amity.ReadReceiptSyncJob | null = this.getSyncJob(channelId);
|
|
213
193
|
|
|
214
194
|
if (syncJob === null || syncJob.syncState === Amity.ReadReceiptSyncState.SYNCING) {
|
|
215
195
|
syncJob = {
|
|
216
|
-
|
|
196
|
+
channelId,
|
|
217
197
|
segment,
|
|
218
198
|
syncState: Amity.ReadReceiptSyncState.CREATED,
|
|
219
199
|
retryCount: 0,
|
|
@@ -225,11 +205,9 @@ export class MessageReadReceiptSyncEngine {
|
|
|
225
205
|
}
|
|
226
206
|
}
|
|
227
207
|
|
|
228
|
-
private getSyncJob(
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const targetJob = syncJobs.find(job => job.subChannelId === subChannelId);
|
|
232
|
-
|
|
208
|
+
private getSyncJob(channelId: string): Amity.ReadReceiptSyncJob | null {
|
|
209
|
+
const { jobQueue } = this;
|
|
210
|
+
const targetJob = jobQueue.find(job => job.channelId === channelId);
|
|
233
211
|
return targetJob || null;
|
|
234
212
|
}
|
|
235
213
|
|
|
@@ -242,13 +220,6 @@ export class MessageReadReceiptSyncEngine {
|
|
|
242
220
|
this.jobQueue.push(syncJob);
|
|
243
221
|
}
|
|
244
222
|
}
|
|
245
|
-
|
|
246
|
-
private removeJobFromQueue(item: Amity.ReadReceiptSyncJob) {
|
|
247
|
-
const index = this.jobQueue.indexOf(item);
|
|
248
|
-
if (index > -1) {
|
|
249
|
-
this.jobQueue.splice(index, 1);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
223
|
}
|
|
253
224
|
|
|
254
225
|
let instance: MessageReadReceiptSyncEngine | null = null;
|
|
@@ -26,6 +26,8 @@ const idResolvers: Resolvers = {
|
|
|
26
26
|
channelUnreadInfo: ({ channelId }) => channelId,
|
|
27
27
|
subChannelUnreadInfo: ({ subChannelId }) => subChannelId,
|
|
28
28
|
|
|
29
|
+
channelUnread: ({ channelId }) => channelId,
|
|
30
|
+
|
|
29
31
|
channelMarker: ({ entityId, userId }) => `${entityId}#${userId}`,
|
|
30
32
|
subChannelMarker: ({ entityId, feedId, userId }) => `${entityId}#${feedId}#${userId}`,
|
|
31
33
|
messageMarker: ({ feedId, contentId, creatorId }) => `${feedId}#${contentId}#${creatorId}`,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getActiveClient } from '~/client/api/activeClient';
|
|
2
|
+
import { createEventSubscriber } from '~/core/events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal used only
|
|
6
|
+
*
|
|
7
|
+
* Fired when an {@link Amity.channelUnreadInfo} has been updated.
|
|
8
|
+
*
|
|
9
|
+
* @param callback The function to call when the event was fired
|
|
10
|
+
* @returns an {@link Amity.Unsubscriber} function to stop listening
|
|
11
|
+
*
|
|
12
|
+
* @category ChannelMarker Events
|
|
13
|
+
*/
|
|
14
|
+
export const onChannelUnreadInfoUpdatedLocal = (
|
|
15
|
+
callback: Amity.Listener<Amity.Events['local.channelUnreadInfo.updated']>,
|
|
16
|
+
): Amity.Unsubscriber => {
|
|
17
|
+
const client = getActiveClient();
|
|
18
|
+
|
|
19
|
+
const filter = (payload: Amity.Events['local.channelUnreadInfo.updated']) => {
|
|
20
|
+
callback(payload);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return createEventSubscriber(
|
|
24
|
+
client,
|
|
25
|
+
'channelMarker/onChannelUnreadInfoUpdatedLocal',
|
|
26
|
+
'local.channelUnreadInfo.updated',
|
|
27
|
+
filter,
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -6,6 +6,7 @@ import { reCalculateChannelUnreadInfo } from '~/marker/utils/reCalculateChannelU
|
|
|
6
6
|
import { getActiveUser } from '~/client/api/activeUser';
|
|
7
7
|
import { markReadMessage } from '../utils/markReadMessage';
|
|
8
8
|
import { prepareMessagePayload } from '../utils';
|
|
9
|
+
import { pullFromCache, pushToCache } from '~/cache/api';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* ```js
|
|
@@ -40,6 +41,39 @@ export const onMessageCreatedMqtt = (
|
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
if (client.useLegacyUnreadCount) {
|
|
45
|
+
rawPayload.messages.forEach(message => {
|
|
46
|
+
const channelUnread = pullFromCache<Amity.ChannelUnread>([
|
|
47
|
+
'channelUnread',
|
|
48
|
+
'get',
|
|
49
|
+
message.channelId,
|
|
50
|
+
])?.data;
|
|
51
|
+
if (!channelUnread || channelUnread.lastSegment >= message.segment) return;
|
|
52
|
+
|
|
53
|
+
const lastSegment = message.segment;
|
|
54
|
+
const isMentionedInMessage = message.mentionedUsers?.some(mention => {
|
|
55
|
+
return (
|
|
56
|
+
mention.type === 'channel' ||
|
|
57
|
+
(mention.type === 'user' &&
|
|
58
|
+
client.userId &&
|
|
59
|
+
mention.userPublicIds.includes(client.userId))
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const lastMentionSegment = isMentionedInMessage
|
|
64
|
+
? message.segment
|
|
65
|
+
: channelUnread.lastMentionSegment;
|
|
66
|
+
|
|
67
|
+
pushToCache(['channelUnread', 'get', message.channelId], {
|
|
68
|
+
...channelUnread,
|
|
69
|
+
lastSegment,
|
|
70
|
+
unreadCount: Math.max(lastSegment - channelUnread.readToSegment, 0),
|
|
71
|
+
lastMentionSegment,
|
|
72
|
+
isMentioned: !(channelUnread.readToSegment >= lastMentionSegment),
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
// Update in cache
|
|
44
78
|
ingestInCache(payload);
|
|
45
79
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { getActiveClient } from '~/client/api/activeClient';
|
|
1
2
|
import ReadReceiptSyncEngine from '~/client/utils/ReadReceiptSync/readReceiptSyncEngine';
|
|
3
|
+
import LegacyReadReceiptSyncEngine from '~/client/utils/ReadReceiptSync/legacyReadReceiptSyncEngine';
|
|
2
4
|
|
|
3
5
|
export const markReadMessage = (message: Amity.InternalMessage) => {
|
|
4
|
-
const
|
|
5
|
-
const markReadReceiptEngine = ReadReceiptSyncEngine.getInstance();
|
|
6
|
+
const client = getActiveClient();
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
if (client.useLegacyUnreadCount) {
|
|
9
|
+
const markReadReceiptEngine = ReadReceiptSyncEngine.getInstance();
|
|
10
|
+
markReadReceiptEngine.markRead(message.channelId, message.channelSegment);
|
|
11
|
+
} else {
|
|
12
|
+
const markReadReceiptEngine = LegacyReadReceiptSyncEngine.getInstance();
|
|
13
|
+
markReadReceiptEngine.markRead(message.subChannelId, message.channelSegment);
|
|
14
|
+
}
|
|
8
15
|
};
|