@amityco/ts-sdk 7.1.0 → 7.1.1-c8d4edca.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 +9 -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/onChannelUnreadUpdatedLocal.d.ts +12 -0
- package/dist/channelRepository/events/onChannelUnreadUpdatedLocal.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/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 +361 -41
- package/dist/index.esm.js +361 -41
- 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/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 +11 -0
- package/src/@types/domains/client.ts +2 -0
- package/src/channelRepository/api/markChannelsAsReadBySegment.ts +29 -0
- package/src/channelRepository/events/onChannelUnreadUpdatedLocal.ts +29 -0
- package/src/channelRepository/observers/getChannel.ts +3 -1
- package/src/channelRepository/observers/getChannels/ChannelLiveCollectionController.ts +6 -1
- 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 +72 -98
- package/src/core/model/idResolvers.ts +2 -0
- package/src/marker/events/onChannelUnreadInfoUpdatedLocal.ts +29 -0
- package/src/marker/events/onChannelUnreadUpdatedLocal.ts +1 -1
- package/src/messageRepository/utils/markReadMessage.ts +10 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal used only
|
|
3
|
+
*
|
|
4
|
+
* Fired when an {@link Amity.userMessageFeedMarkers} has been resolved by Object Rsesolver
|
|
5
|
+
*
|
|
6
|
+
* @param callback The function to call when the event was fired
|
|
7
|
+
* @returns an {@link Amity.Unsubscriber} function to stop listening
|
|
8
|
+
*
|
|
9
|
+
* @category MessageMarker Events
|
|
10
|
+
*/
|
|
11
|
+
export declare const onChannelUnreadInfoUpdatedLocal: (callback: Amity.Listener<Amity.Events['local.channelUnreadInfo.updated']>) => Amity.Unsubscriber;
|
|
12
|
+
//# sourceMappingURL=onChannelUnreadInfoUpdatedLocal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onChannelUnreadInfoUpdatedLocal.d.ts","sourceRoot":"","sources":["../../../src/marker/events/onChannelUnreadInfoUpdatedLocal.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,aAChC,MAAM,QAAQ,CAAC,MAAM,MAAM,CAAC,iCAAiC,CAAC,CAAC,KACxE,MAAM,YAaR,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markReadMessage.d.ts","sourceRoot":"","sources":["../../../src/messageRepository/utils/markReadMessage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"markReadMessage.d.ts","sourceRoot":"","sources":["../../../src/messageRepository/utils/markReadMessage.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,YAAa,MAAM,eAAe,SAU7D,CAAC"}
|
package/package.json
CHANGED
|
@@ -237,7 +237,8 @@ declare global {
|
|
|
237
237
|
};
|
|
238
238
|
|
|
239
239
|
'local.subChannelUnread.updated': Amity.SubChannelUnreadInfo;
|
|
240
|
-
'local.
|
|
240
|
+
'local.channelUnreadInfo.updated': Amity.ChannelUnreadInfo;
|
|
241
|
+
'local.channelUnread.updated': Amity.ChannelUnread;
|
|
241
242
|
|
|
242
243
|
'local.story.created': Amity.StoryPayload;
|
|
243
244
|
'local.story.updated': Amity.StoryPayload;
|
package/src/@types/core/model.ts
CHANGED
|
@@ -25,6 +25,8 @@ declare global {
|
|
|
25
25
|
channelUnreadInfo: Amity.ChannelUnreadInfo;
|
|
26
26
|
subChannelUnreadInfo: Amity.SubChannelUnreadInfo;
|
|
27
27
|
|
|
28
|
+
channelUnread: Amity.ChannelUnread;
|
|
29
|
+
|
|
28
30
|
channelMarker: Amity.ChannelMarker;
|
|
29
31
|
subChannelMarker: Amity.SubChannelMarker;
|
|
30
32
|
messageMarker: Amity.MessageMarker;
|
|
@@ -84,6 +86,8 @@ declare global {
|
|
|
84
86
|
channelUnreadInfo: Pick<Amity.ChannelUnreadInfo, 'channelId'>;
|
|
85
87
|
subChannelUnreadInfo: Pick<Amity.SubChannelUnreadInfo, 'subChannelId'>;
|
|
86
88
|
|
|
89
|
+
channelUnread: Pick<Amity.ChannelUnread, 'channelId'>;
|
|
90
|
+
|
|
87
91
|
channelMarker: Pick<Amity.ChannelMarker, 'entityId' | 'userId'>;
|
|
88
92
|
subChannelMarker: Pick<Amity.SubChannelMarker, 'feedId' | 'entityId' | 'userId'>;
|
|
89
93
|
messageMarker: Pick<Amity.MessageMarker, 'feedId' | 'contentId' | 'creatorId'>;
|
|
@@ -8,12 +8,25 @@ declare global {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
type ReadReceipt = {
|
|
11
|
-
|
|
11
|
+
channelId: Amity.Channel['channelId'];
|
|
12
12
|
latestSegment: number;
|
|
13
13
|
latestSyncSegment: number;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
type ReadReceiptSyncJob = {
|
|
17
|
+
channelId: Amity.Channel['channelId'];
|
|
18
|
+
segment: number;
|
|
19
|
+
syncState: ReadReceiptSyncState;
|
|
20
|
+
retryCount: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type LegacyReadReceipt = {
|
|
24
|
+
subChannelId: Amity.SubChannel['subChannelId'];
|
|
25
|
+
latestSegment: number;
|
|
26
|
+
latestSyncSegment: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type LegacyReadReceiptSyncJob = {
|
|
17
30
|
subChannelId: Amity.SubChannel['subChannelId'];
|
|
18
31
|
segment: number;
|
|
19
32
|
syncState: ReadReceiptSyncState;
|
|
@@ -137,5 +137,16 @@ declare global {
|
|
|
137
137
|
Amity.Membership<'channel'>['userId'],
|
|
138
138
|
Pick<QueryChannelMembers, 'page'>
|
|
139
139
|
>;
|
|
140
|
+
|
|
141
|
+
// Use for channel's unread count value stored in the local cache
|
|
142
|
+
type ChannelUnread = {
|
|
143
|
+
channelId: Amity.Channel['channelId'];
|
|
144
|
+
unreadCount: number;
|
|
145
|
+
isMentioned: boolean;
|
|
146
|
+
readToSegment: number;
|
|
147
|
+
lastSegment: number;
|
|
148
|
+
lastMentionSegment: number;
|
|
149
|
+
isDeleted: boolean;
|
|
150
|
+
};
|
|
140
151
|
}
|
|
141
152
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getActiveClient } from '~/client/api/activeClient';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* Mark subChannel as read by readToSegment
|
|
6
|
+
*
|
|
7
|
+
* @param subChannelIds the IDs of the {@link Amity.SubChannel} to update
|
|
8
|
+
* @param readToSegment the segment to mark as read
|
|
9
|
+
* @returns a success boolean if the {@link Amity.SubChannel} was updated
|
|
10
|
+
*
|
|
11
|
+
* @category Channel API
|
|
12
|
+
* @async
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const markChannelsAsReadBySegment = async (
|
|
16
|
+
readings: {
|
|
17
|
+
channelId: Amity.Channel['channelId'];
|
|
18
|
+
readToSegment: number;
|
|
19
|
+
}[],
|
|
20
|
+
): Promise<boolean> => {
|
|
21
|
+
const client = getActiveClient();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await client.http.post<Amity.MarkAsReadPayload>('api/v3/channels/seen', { channels: readings });
|
|
25
|
+
return true;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -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.userMessageFeedMarkers} has been resolved by Object Rsesolver
|
|
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 MessageMarker Events
|
|
13
|
+
*/
|
|
14
|
+
export const onChannelUnreadUpdatedLocal = (
|
|
15
|
+
callback: Amity.Listener<Amity.Events['local.channelUnread.updated']>,
|
|
16
|
+
): Amity.Unsubscriber => {
|
|
17
|
+
const client = getActiveClient();
|
|
18
|
+
|
|
19
|
+
const filter = (payload: Amity.Events['local.channelUnread.updated']) => {
|
|
20
|
+
callback(payload);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return createEventSubscriber(
|
|
24
|
+
client,
|
|
25
|
+
'channel/onChannelUnreadUpdatedLocal',
|
|
26
|
+
'local.channelUnread.updated',
|
|
27
|
+
filter,
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -32,8 +32,9 @@ import { isEqual } from '~/utils/isEqual';
|
|
|
32
32
|
import { updateChannelCache } from '../utils/updateChannelCache';
|
|
33
33
|
import { onChannelMarkerUpdated } from '~/marker/events/onChannelMarkerUpdated';
|
|
34
34
|
import { onSubChannelCreated } from '~/subChannelRepository';
|
|
35
|
-
import {
|
|
35
|
+
import { onChannelUnreadInfoUpdatedLocal } from '~/marker/events/onChannelUnreadInfoUpdatedLocal';
|
|
36
36
|
import { constructChannelObject } from '../utils/constructChannelObject';
|
|
37
|
+
import { onChannelUnreadUpdatedLocal } from '../events/onChannelUnreadUpdatedLocal';
|
|
37
38
|
|
|
38
39
|
/* begin_public_function
|
|
39
40
|
id: channel.get
|
|
@@ -295,6 +296,7 @@ export const getChannel = (
|
|
|
295
296
|
'channel',
|
|
296
297
|
),
|
|
297
298
|
convertEventPayload(onSubChannelCreated, 'channelId', 'channel'),
|
|
299
|
+
convertEventPayload(onChannelUnreadInfoUpdatedLocal, 'channelId', 'channel'),
|
|
298
300
|
convertEventPayload(onChannelUnreadUpdatedLocal, 'channelId', 'channel'),
|
|
299
301
|
],
|
|
300
302
|
{
|
|
@@ -55,8 +55,9 @@ import { prepareUnreadCountInfo } from '~/channelRepository/utils/prepareUnreadC
|
|
|
55
55
|
import { resolveUnreadInfoOnChannelEvent } from '~/channelRepository/utils/resolveUnreadInfoOnChannelEvent';
|
|
56
56
|
import { onChannelResolved } from '~/channelRepository/events/onChannelResolved';
|
|
57
57
|
import { onUserMessageFeedMarkerResolved } from '~/marker/events/onUserMessageFeedMarkerResolved';
|
|
58
|
-
import {
|
|
58
|
+
import { onChannelUnreadInfoUpdatedLocal } from '~/marker/events/onChannelUnreadInfoUpdatedLocal';
|
|
59
59
|
import { constructChannelObject } from '~/channelRepository/utils/constructChannelObject';
|
|
60
|
+
import { onChannelUnreadUpdatedLocal } from '~/channelRepository/events/onChannelUnreadUpdatedLocal';
|
|
60
61
|
|
|
61
62
|
export class ChannelLiveCollectionController extends LiveCollectionController<
|
|
62
63
|
'channel',
|
|
@@ -566,6 +567,10 @@ export class ChannelLiveCollectionController extends LiveCollectionController<
|
|
|
566
567
|
},
|
|
567
568
|
action: Amity.ChannelActionType.OnResolveUnread,
|
|
568
569
|
},
|
|
570
|
+
{
|
|
571
|
+
fn: convertEventPayload(onChannelUnreadInfoUpdatedLocal, 'channelId', 'channel'),
|
|
572
|
+
action: Amity.ChannelActionType.OnUpdate,
|
|
573
|
+
},
|
|
569
574
|
{
|
|
570
575
|
fn: convertEventPayload(onChannelUnreadUpdatedLocal, 'channelId', 'channel'),
|
|
571
576
|
action: Amity.ChannelActionType.OnUpdate,
|
|
@@ -1,17 +1,27 @@
|
|
|
1
|
+
import { get } from 'http';
|
|
1
2
|
import { shallowClone } from '~/utils/shallowClone';
|
|
2
3
|
import { getChannelIsMentioned } from './getChannelIsMentioned';
|
|
3
4
|
import { getSubChannelsUnreadCount } from './getSubChannelsUnreadCount';
|
|
5
|
+
import { getActiveClient } from '~/client/api/activeClient';
|
|
6
|
+
import { getLegacyChannelUnread } from './getLegacyChannelUnread';
|
|
4
7
|
|
|
5
8
|
export const constructChannelDynamicValue = (
|
|
6
9
|
channel: Amity.StaticInternalChannel,
|
|
7
10
|
): Amity.InternalChannel => {
|
|
11
|
+
const client = getActiveClient();
|
|
8
12
|
const { messageCount, ...rest } = channel;
|
|
13
|
+
|
|
9
14
|
return shallowClone(rest, {
|
|
10
|
-
get
|
|
11
|
-
return
|
|
15
|
+
get unreadCount() {
|
|
16
|
+
return getLegacyChannelUnread(rest.channelId)?.unreadCount ?? 0;
|
|
12
17
|
},
|
|
13
18
|
get subChannelsUnreadCount() {
|
|
14
19
|
return getSubChannelsUnreadCount(rest);
|
|
15
20
|
},
|
|
21
|
+
get isMentioned() {
|
|
22
|
+
if (client.useLegacyUnreadCount)
|
|
23
|
+
return getLegacyChannelUnread(rest.channelId)?.isMentioned ?? false;
|
|
24
|
+
return getChannelIsMentioned(rest);
|
|
25
|
+
},
|
|
16
26
|
});
|
|
17
27
|
};
|
|
@@ -5,9 +5,8 @@ import { getChannelMarkers } from '~/marker/api/getChannelMarkers';
|
|
|
5
5
|
import { updateChannelMessagePreviewCache } from '~/messagePreview/utils';
|
|
6
6
|
import { getActiveClient } from '~/client/api/activeClient';
|
|
7
7
|
import { pullFromCache } from '~/cache/api/pullFromCache';
|
|
8
|
-
import { getSubChannelsUnreadCount } from './getSubChannelsUnreadCount';
|
|
9
|
-
import { getChannelIsMentioned } from './getChannelIsMentioned';
|
|
10
8
|
import { convertRawUserToInternalUser } from '~/userRepository/utils/convertRawUserToInternalUser';
|
|
9
|
+
import { pushToCache } from '~/cache/api';
|
|
11
10
|
|
|
12
11
|
export const MARKER_INCLUDED_CHANNEL_TYPE = ['broadcast', 'conversation', 'community'];
|
|
13
12
|
export const isUnreadCountSupport = ({ type }: Pick<Amity.RawChannel, 'type'>) =>
|
|
@@ -48,6 +47,37 @@ export const preUpdateChannelCache = (
|
|
|
48
47
|
});
|
|
49
48
|
};
|
|
50
49
|
|
|
50
|
+
const updateChannelUnread = ({
|
|
51
|
+
currentUserId,
|
|
52
|
+
channels,
|
|
53
|
+
channelUsers,
|
|
54
|
+
}: {
|
|
55
|
+
currentUserId: Amity.User['userId'];
|
|
56
|
+
channels: Amity.RawChannel[];
|
|
57
|
+
channelUsers: Amity.RawMembership<'channel'>[];
|
|
58
|
+
}) => {
|
|
59
|
+
for (let i = 0; i < channels.length; i += 1) {
|
|
60
|
+
const cacheKey = ['channelUnread', 'get', channels[i].channelId];
|
|
61
|
+
const { readToSegment, lastMentionedSegment } = channelUsers.find(
|
|
62
|
+
channelUser =>
|
|
63
|
+
channelUser.channelId === channels[i].channelId && channelUser.userId === currentUserId,
|
|
64
|
+
) || {
|
|
65
|
+
readToSegment: 0,
|
|
66
|
+
lastMentionedSegment: 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
pushToCache(cacheKey, {
|
|
70
|
+
channelId: channels[i].channelId,
|
|
71
|
+
lastSegment: channels[i].messageCount,
|
|
72
|
+
readToSegment,
|
|
73
|
+
lastMentionedSegment,
|
|
74
|
+
unreadCount: channels[i].messageCount - readToSegment,
|
|
75
|
+
isMentioned: lastMentionedSegment > readToSegment,
|
|
76
|
+
isDeleted: channels[i].isDeleted,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
51
81
|
export const prepareChannelPayload = async (
|
|
52
82
|
rawPayload: Amity.ChannelPayload,
|
|
53
83
|
options: { isMessagePreviewUpdated?: boolean } = { isMessagePreviewUpdated: true },
|
|
@@ -64,28 +94,38 @@ export const prepareChannelPayload = async (
|
|
|
64
94
|
updateChannelMessagePreviewCache(rawPayload);
|
|
65
95
|
}
|
|
66
96
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
if (client.useLegacyUnreadCount) {
|
|
98
|
+
updateChannelUnread({
|
|
99
|
+
channels: rawPayload.channels,
|
|
100
|
+
channelUsers: rawPayload.channelUsers,
|
|
101
|
+
currentUserId: client.userId!,
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
const markerIds = rawPayload.channels
|
|
105
|
+
// filter channel by type. Only conversation, community and broadcast type are included.
|
|
106
|
+
.filter(isUnreadCountSupport)
|
|
107
|
+
.map(({ channelInternalId }) => channelInternalId);
|
|
108
|
+
|
|
109
|
+
if (markerIds.length > 0) {
|
|
110
|
+
// since the get markers method requires a channel cache to function with the reducer.
|
|
111
|
+
preUpdateChannelCache(rawPayload, {
|
|
112
|
+
isMessagePreviewUpdated: options.isMessagePreviewUpdated,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await getChannelMarkers(markerIds);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// empty block (from the spec, allow marker fetch to fail without having to do anything)
|
|
119
|
+
}
|
|
80
120
|
}
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
//
|
|
123
|
+
// convert raw channel to internal channel
|
|
84
124
|
const channels = rawPayload.channels.map(payload =>
|
|
85
125
|
convertFromRaw(payload, { isMessagePreviewUpdated: options.isMessagePreviewUpdated }),
|
|
86
126
|
);
|
|
87
127
|
|
|
88
|
-
// user
|
|
128
|
+
// convert raw channel user to membership (add user object)
|
|
89
129
|
const channelUsers: Amity.Membership<'channel'>[] = rawPayload.channelUsers.map(channelUser => {
|
|
90
130
|
return convertRawMembershipToMembership<'channel'>(channelUser);
|
|
91
131
|
});
|
|
@@ -86,6 +86,8 @@ export const createClient = (
|
|
|
86
86
|
const sessionHandler = undefined;
|
|
87
87
|
|
|
88
88
|
const isUnreadCountEnabled = false;
|
|
89
|
+
// Legacy unread count is true by default
|
|
90
|
+
const useLegacyUnreadCount = true;
|
|
89
91
|
|
|
90
92
|
const client = {
|
|
91
93
|
version: `${VERSION}`,
|
|
@@ -122,6 +124,7 @@ export const createClient = (
|
|
|
122
124
|
use: () => setActiveClient(client),
|
|
123
125
|
|
|
124
126
|
isUnreadCountEnabled,
|
|
127
|
+
useLegacyUnreadCount,
|
|
125
128
|
|
|
126
129
|
getMarkerSyncConsistentMode,
|
|
127
130
|
|
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
|
+
};
|