@quilibrium/quorum-shared 2.1.0-1
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/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query key factories for React Query
|
|
3
|
+
*
|
|
4
|
+
* Consistent key structure across platforms for cache management
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const queryKeys = {
|
|
8
|
+
// Spaces
|
|
9
|
+
spaces: {
|
|
10
|
+
all: ['spaces'] as const,
|
|
11
|
+
detail: (spaceId: string) => ['spaces', spaceId] as const,
|
|
12
|
+
members: (spaceId: string) => ['spaces', spaceId, 'members'] as const,
|
|
13
|
+
member: (spaceId: string, address: string) =>
|
|
14
|
+
['spaces', spaceId, 'members', address] as const,
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Channels
|
|
18
|
+
channels: {
|
|
19
|
+
bySpace: (spaceId: string) => ['channels', spaceId] as const,
|
|
20
|
+
detail: (spaceId: string, channelId: string) =>
|
|
21
|
+
['channels', spaceId, channelId] as const,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Messages
|
|
25
|
+
messages: {
|
|
26
|
+
infinite: (spaceId: string, channelId: string) =>
|
|
27
|
+
['messages', 'infinite', spaceId, channelId] as const,
|
|
28
|
+
detail: (spaceId: string, channelId: string, messageId: string) =>
|
|
29
|
+
['messages', spaceId, channelId, messageId] as const,
|
|
30
|
+
pinned: (spaceId: string, channelId: string) =>
|
|
31
|
+
['messages', 'pinned', spaceId, channelId] as const,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Conversations (DMs)
|
|
35
|
+
conversations: {
|
|
36
|
+
all: (type: 'direct' | 'group') => ['conversations', type] as const,
|
|
37
|
+
detail: (conversationId: string) => ['conversations', conversationId] as const,
|
|
38
|
+
messages: (conversationId: string) =>
|
|
39
|
+
['conversations', conversationId, 'messages'] as const,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// User
|
|
43
|
+
user: {
|
|
44
|
+
config: (address: string) => ['user', 'config', address] as const,
|
|
45
|
+
profile: (address: string) => ['user', 'profile', address] as const,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Bookmarks
|
|
49
|
+
bookmarks: {
|
|
50
|
+
all: ['bookmarks'] as const,
|
|
51
|
+
bySource: (sourceType: 'channel' | 'dm') =>
|
|
52
|
+
['bookmarks', sourceType] as const,
|
|
53
|
+
bySpace: (spaceId: string) => ['bookmarks', 'space', spaceId] as const,
|
|
54
|
+
check: (messageId: string) => ['bookmarks', 'check', messageId] as const,
|
|
55
|
+
},
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
// Type helpers for extracting key types
|
|
59
|
+
export type SpacesKey = typeof queryKeys.spaces.all;
|
|
60
|
+
export type SpaceDetailKey = ReturnType<typeof queryKeys.spaces.detail>;
|
|
61
|
+
export type ChannelsKey = ReturnType<typeof queryKeys.channels.bySpace>;
|
|
62
|
+
export type MessagesInfiniteKey = ReturnType<typeof queryKeys.messages.infinite>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation hooks exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useSendMessage } from './useSendMessage';
|
|
6
|
+
export type { UseSendMessageOptions } from './useSendMessage';
|
|
7
|
+
|
|
8
|
+
export { useAddReaction, useRemoveReaction } from './useReaction';
|
|
9
|
+
export type { UseReactionOptions } from './useReaction';
|
|
10
|
+
|
|
11
|
+
export { useEditMessage } from './useEditMessage';
|
|
12
|
+
export type { UseEditMessageOptions } from './useEditMessage';
|
|
13
|
+
|
|
14
|
+
export { useDeleteMessage } from './useDeleteMessage';
|
|
15
|
+
export type { UseDeleteMessageOptions } from './useDeleteMessage';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDeleteMessage mutation hook
|
|
3
|
+
*
|
|
4
|
+
* Delete a message with optimistic updates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import type { StorageAdapter, GetMessagesResult } from '../../storage';
|
|
9
|
+
import type { QuorumApiClient, DeleteMessageParams } from '../../api';
|
|
10
|
+
import { queryKeys } from '../keys';
|
|
11
|
+
|
|
12
|
+
export interface UseDeleteMessageOptions {
|
|
13
|
+
storage: StorageAdapter;
|
|
14
|
+
apiClient: QuorumApiClient;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useDeleteMessage({ storage, apiClient }: UseDeleteMessageOptions) {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
|
|
20
|
+
return useMutation({
|
|
21
|
+
mutationFn: async (params: DeleteMessageParams): Promise<void> => {
|
|
22
|
+
return apiClient.deleteMessage(params);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
onMutate: async (params) => {
|
|
26
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
27
|
+
|
|
28
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
29
|
+
const previousData = queryClient.getQueryData(key);
|
|
30
|
+
|
|
31
|
+
// Optimistically remove message
|
|
32
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
33
|
+
if (!old) return old;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
...old,
|
|
37
|
+
pages: old.pages.map((page) => ({
|
|
38
|
+
...page,
|
|
39
|
+
messages: page.messages.filter(
|
|
40
|
+
(message) => message.messageId !== params.messageId
|
|
41
|
+
),
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return { previousData };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
onError: (err, params, context) => {
|
|
50
|
+
if (context?.previousData) {
|
|
51
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
52
|
+
queryClient.setQueryData(key, context.previousData);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
onSuccess: async (_, params) => {
|
|
57
|
+
// Remove from storage
|
|
58
|
+
await storage.deleteMessage(params.messageId);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
onSettled: (data, err, params) => {
|
|
62
|
+
queryClient.invalidateQueries({
|
|
63
|
+
queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId),
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEditMessage mutation hook
|
|
3
|
+
*
|
|
4
|
+
* Edit a message with optimistic updates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import type { Message, PostMessage } from '../../types';
|
|
9
|
+
import type { StorageAdapter, GetMessagesResult } from '../../storage';
|
|
10
|
+
import type { QuorumApiClient, EditMessageParams } from '../../api';
|
|
11
|
+
import { queryKeys } from '../keys';
|
|
12
|
+
|
|
13
|
+
export interface UseEditMessageOptions {
|
|
14
|
+
storage: StorageAdapter;
|
|
15
|
+
apiClient: QuorumApiClient;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useEditMessage({ storage, apiClient }: UseEditMessageOptions) {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutationFn: async (params: EditMessageParams): Promise<Message> => {
|
|
23
|
+
return apiClient.editMessage(params);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
onMutate: async (params) => {
|
|
27
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
28
|
+
|
|
29
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
30
|
+
const previousData = queryClient.getQueryData(key);
|
|
31
|
+
|
|
32
|
+
// Optimistically update message
|
|
33
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
34
|
+
if (!old) return old;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...old,
|
|
38
|
+
pages: old.pages.map((page) => ({
|
|
39
|
+
...page,
|
|
40
|
+
messages: page.messages.map((message) => {
|
|
41
|
+
if (message.messageId !== params.messageId) return message;
|
|
42
|
+
|
|
43
|
+
// Only update if it's a post message
|
|
44
|
+
if (message.content.type !== 'post') return message;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...message,
|
|
48
|
+
modifiedDate: Date.now(),
|
|
49
|
+
content: {
|
|
50
|
+
...message.content,
|
|
51
|
+
text: params.text,
|
|
52
|
+
} as PostMessage,
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return { previousData };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
onError: (err, params, context) => {
|
|
63
|
+
if (context?.previousData) {
|
|
64
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
65
|
+
queryClient.setQueryData(key, context.previousData);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
onSuccess: async (message) => {
|
|
70
|
+
// Persist updated message to storage
|
|
71
|
+
await storage.saveMessage(
|
|
72
|
+
message,
|
|
73
|
+
message.modifiedDate,
|
|
74
|
+
message.content.senderId,
|
|
75
|
+
'space',
|
|
76
|
+
'',
|
|
77
|
+
''
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
onSettled: (data, err, params) => {
|
|
82
|
+
queryClient.invalidateQueries({
|
|
83
|
+
queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReaction mutation hooks
|
|
3
|
+
*
|
|
4
|
+
* Add and remove reactions with optimistic updates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import type { Message, Reaction } from '../../types';
|
|
9
|
+
import type { StorageAdapter, GetMessagesResult } from '../../storage';
|
|
10
|
+
import type { QuorumApiClient, AddReactionParams, RemoveReactionParams } from '../../api';
|
|
11
|
+
import { queryKeys } from '../keys';
|
|
12
|
+
|
|
13
|
+
export interface UseReactionOptions {
|
|
14
|
+
storage: StorageAdapter;
|
|
15
|
+
apiClient: QuorumApiClient;
|
|
16
|
+
currentUserId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useAddReaction({
|
|
20
|
+
storage,
|
|
21
|
+
apiClient,
|
|
22
|
+
currentUserId,
|
|
23
|
+
}: UseReactionOptions) {
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
|
|
26
|
+
return useMutation({
|
|
27
|
+
mutationFn: async (params: AddReactionParams): Promise<void> => {
|
|
28
|
+
return apiClient.addReaction(params);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
onMutate: async (params) => {
|
|
32
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
33
|
+
|
|
34
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
35
|
+
const previousData = queryClient.getQueryData(key);
|
|
36
|
+
|
|
37
|
+
// Optimistically add reaction
|
|
38
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
39
|
+
if (!old) return old;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...old,
|
|
43
|
+
pages: old.pages.map((page) => ({
|
|
44
|
+
...page,
|
|
45
|
+
messages: page.messages.map((message) => {
|
|
46
|
+
if (message.messageId !== params.messageId) return message;
|
|
47
|
+
|
|
48
|
+
const existingReaction = message.reactions.find(
|
|
49
|
+
(r) => r.emojiName === params.reaction
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (existingReaction) {
|
|
53
|
+
// Add user to existing reaction
|
|
54
|
+
return {
|
|
55
|
+
...message,
|
|
56
|
+
reactions: message.reactions.map((r) =>
|
|
57
|
+
r.emojiName === params.reaction
|
|
58
|
+
? {
|
|
59
|
+
...r,
|
|
60
|
+
count: r.count + 1,
|
|
61
|
+
memberIds: [...r.memberIds, currentUserId],
|
|
62
|
+
}
|
|
63
|
+
: r
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
// Create new reaction
|
|
68
|
+
const newReaction: Reaction = {
|
|
69
|
+
emojiId: params.reaction,
|
|
70
|
+
emojiName: params.reaction,
|
|
71
|
+
spaceId: params.spaceId,
|
|
72
|
+
count: 1,
|
|
73
|
+
memberIds: [currentUserId],
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
...message,
|
|
77
|
+
reactions: [...message.reactions, newReaction],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { previousData };
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
onError: (err, params, context) => {
|
|
89
|
+
if (context?.previousData) {
|
|
90
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
91
|
+
queryClient.setQueryData(key, context.previousData);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useRemoveReaction({
|
|
98
|
+
storage,
|
|
99
|
+
apiClient,
|
|
100
|
+
currentUserId,
|
|
101
|
+
}: UseReactionOptions) {
|
|
102
|
+
const queryClient = useQueryClient();
|
|
103
|
+
|
|
104
|
+
return useMutation({
|
|
105
|
+
mutationFn: async (params: RemoveReactionParams): Promise<void> => {
|
|
106
|
+
return apiClient.removeReaction(params);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
onMutate: async (params) => {
|
|
110
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
111
|
+
|
|
112
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
113
|
+
const previousData = queryClient.getQueryData(key);
|
|
114
|
+
|
|
115
|
+
// Optimistically remove reaction
|
|
116
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
117
|
+
if (!old) return old;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...old,
|
|
121
|
+
pages: old.pages.map((page) => ({
|
|
122
|
+
...page,
|
|
123
|
+
messages: page.messages.map((message) => {
|
|
124
|
+
if (message.messageId !== params.messageId) return message;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...message,
|
|
128
|
+
reactions: message.reactions
|
|
129
|
+
.map((r) => {
|
|
130
|
+
if (r.emojiName !== params.reaction) return r;
|
|
131
|
+
|
|
132
|
+
const newMemberIds = r.memberIds.filter(
|
|
133
|
+
(id) => id !== currentUserId
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (newMemberIds.length === 0) {
|
|
137
|
+
return null; // Remove reaction entirely
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...r,
|
|
142
|
+
count: newMemberIds.length,
|
|
143
|
+
memberIds: newMemberIds,
|
|
144
|
+
};
|
|
145
|
+
})
|
|
146
|
+
.filter((r): r is Reaction => r !== null),
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
})),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return { previousData };
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
onError: (err, params, context) => {
|
|
157
|
+
if (context?.previousData) {
|
|
158
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
159
|
+
queryClient.setQueryData(key, context.previousData);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSendMessage mutation hook
|
|
3
|
+
*
|
|
4
|
+
* Sends a message with optimistic updates
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import type { Message } from '../../types';
|
|
9
|
+
import type { StorageAdapter, GetMessagesResult } from '../../storage';
|
|
10
|
+
import type { QuorumApiClient, SendMessageParams } from '../../api';
|
|
11
|
+
import { queryKeys } from '../keys';
|
|
12
|
+
|
|
13
|
+
export interface UseSendMessageOptions {
|
|
14
|
+
storage: StorageAdapter;
|
|
15
|
+
apiClient: QuorumApiClient;
|
|
16
|
+
currentUserId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useSendMessage({
|
|
20
|
+
storage,
|
|
21
|
+
apiClient,
|
|
22
|
+
currentUserId,
|
|
23
|
+
}: UseSendMessageOptions) {
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
|
|
26
|
+
return useMutation({
|
|
27
|
+
mutationFn: async (params: SendMessageParams): Promise<Message> => {
|
|
28
|
+
return apiClient.sendMessage(params);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
onMutate: async (params) => {
|
|
32
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
33
|
+
|
|
34
|
+
// Cancel outgoing refetches
|
|
35
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
36
|
+
|
|
37
|
+
// Snapshot previous value
|
|
38
|
+
const previousData = queryClient.getQueryData(key);
|
|
39
|
+
|
|
40
|
+
// Create optimistic message
|
|
41
|
+
const optimisticMessage: Message = {
|
|
42
|
+
messageId: `temp-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
43
|
+
channelId: params.channelId,
|
|
44
|
+
spaceId: params.spaceId,
|
|
45
|
+
digestAlgorithm: '',
|
|
46
|
+
nonce: '',
|
|
47
|
+
createdDate: Date.now(),
|
|
48
|
+
modifiedDate: Date.now(),
|
|
49
|
+
lastModifiedHash: '',
|
|
50
|
+
content: {
|
|
51
|
+
type: 'post',
|
|
52
|
+
senderId: currentUserId,
|
|
53
|
+
text: params.text,
|
|
54
|
+
repliesToMessageId: params.repliesToMessageId,
|
|
55
|
+
},
|
|
56
|
+
reactions: [],
|
|
57
|
+
mentions: { memberIds: [], roleIds: [], channelIds: [] },
|
|
58
|
+
sendStatus: 'sending',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Optimistically add to cache
|
|
62
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
63
|
+
if (!old) return old;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...old,
|
|
67
|
+
pages: old.pages.map((page, index) => {
|
|
68
|
+
if (index === 0) {
|
|
69
|
+
return {
|
|
70
|
+
...page,
|
|
71
|
+
messages: [optimisticMessage, ...page.messages],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return page;
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return { previousData, optimisticMessage };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
onError: (err, params, context) => {
|
|
83
|
+
// Rollback on error
|
|
84
|
+
if (context?.previousData) {
|
|
85
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
86
|
+
queryClient.setQueryData(key, context.previousData);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
onSuccess: async (message, params, context) => {
|
|
91
|
+
// Replace optimistic message with real one
|
|
92
|
+
const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
|
|
93
|
+
|
|
94
|
+
queryClient.setQueryData(key, (old: { pages: GetMessagesResult[]; pageParams: unknown[] } | undefined) => {
|
|
95
|
+
if (!old) return old;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...old,
|
|
99
|
+
pages: old.pages.map((page, index) => {
|
|
100
|
+
if (index === 0) {
|
|
101
|
+
return {
|
|
102
|
+
...page,
|
|
103
|
+
messages: page.messages.map((m) =>
|
|
104
|
+
m.messageId === context?.optimisticMessage.messageId ? message : m
|
|
105
|
+
),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return page;
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Persist to storage
|
|
114
|
+
await storage.saveMessage(
|
|
115
|
+
message,
|
|
116
|
+
message.createdDate,
|
|
117
|
+
currentUserId,
|
|
118
|
+
'space',
|
|
119
|
+
'',
|
|
120
|
+
''
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
onSettled: (data, err, params) => {
|
|
125
|
+
// Always refetch after mutation
|
|
126
|
+
queryClient.invalidateQueries({
|
|
127
|
+
queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useChannels hook
|
|
3
|
+
*
|
|
4
|
+
* Fetches and caches channel data for a space
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useQuery } from '@tanstack/react-query';
|
|
8
|
+
import type { Channel } from '../types';
|
|
9
|
+
import type { StorageAdapter } from '../storage';
|
|
10
|
+
import { queryKeys } from './keys';
|
|
11
|
+
|
|
12
|
+
export interface UseChannelsOptions {
|
|
13
|
+
storage: StorageAdapter;
|
|
14
|
+
spaceId: string | undefined;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useChannels({ storage, spaceId, enabled = true }: UseChannelsOptions) {
|
|
19
|
+
return useQuery({
|
|
20
|
+
queryKey: queryKeys.channels.bySpace(spaceId ?? ''),
|
|
21
|
+
queryFn: async (): Promise<Channel[]> => {
|
|
22
|
+
if (!spaceId) return [];
|
|
23
|
+
return storage.getChannels(spaceId);
|
|
24
|
+
},
|
|
25
|
+
enabled: enabled && !!spaceId,
|
|
26
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper to extract channels from a space's groups
|
|
32
|
+
*/
|
|
33
|
+
export function flattenChannels(groups: { channels: Channel[] }[]): Channel[] {
|
|
34
|
+
return groups.flatMap((group) => group.channels);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find a channel by ID within groups
|
|
39
|
+
*/
|
|
40
|
+
export function findChannel(
|
|
41
|
+
groups: { channels: Channel[] }[],
|
|
42
|
+
channelId: string
|
|
43
|
+
): Channel | undefined {
|
|
44
|
+
for (const group of groups) {
|
|
45
|
+
const channel = group.channels.find((c) => c.channelId === channelId);
|
|
46
|
+
if (channel) return channel;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMessages hook
|
|
3
|
+
*
|
|
4
|
+
* Infinite query for paginated message loading
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import type { Message } from '../types';
|
|
9
|
+
import type { StorageAdapter, GetMessagesResult } from '../storage';
|
|
10
|
+
import { queryKeys } from './keys';
|
|
11
|
+
|
|
12
|
+
export interface UseMessagesOptions {
|
|
13
|
+
storage: StorageAdapter;
|
|
14
|
+
spaceId: string | undefined;
|
|
15
|
+
channelId: string | undefined;
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useMessages({
|
|
21
|
+
storage,
|
|
22
|
+
spaceId,
|
|
23
|
+
channelId,
|
|
24
|
+
enabled = true,
|
|
25
|
+
limit = 50,
|
|
26
|
+
}: UseMessagesOptions) {
|
|
27
|
+
return useInfiniteQuery({
|
|
28
|
+
queryKey: queryKeys.messages.infinite(spaceId ?? '', channelId ?? ''),
|
|
29
|
+
queryFn: async ({ pageParam }): Promise<GetMessagesResult> => {
|
|
30
|
+
if (!spaceId || !channelId) {
|
|
31
|
+
return { messages: [], nextCursor: null, prevCursor: null };
|
|
32
|
+
}
|
|
33
|
+
return storage.getMessages({
|
|
34
|
+
spaceId,
|
|
35
|
+
channelId,
|
|
36
|
+
cursor: pageParam as number | undefined,
|
|
37
|
+
direction: 'backward',
|
|
38
|
+
limit,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
getNextPageParam: (lastPage) => lastPage.prevCursor,
|
|
42
|
+
getPreviousPageParam: (firstPage) => firstPage.nextCursor,
|
|
43
|
+
initialPageParam: undefined as number | undefined,
|
|
44
|
+
enabled: enabled && !!spaceId && !!channelId,
|
|
45
|
+
staleTime: 1000 * 30, // 30 seconds
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Flatten paginated messages into a single array
|
|
51
|
+
*/
|
|
52
|
+
export function flattenMessages(
|
|
53
|
+
pages: GetMessagesResult[] | undefined
|
|
54
|
+
): Message[] {
|
|
55
|
+
if (!pages) return [];
|
|
56
|
+
return pages.flatMap((page) => page.messages);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hook to invalidate message cache
|
|
61
|
+
*/
|
|
62
|
+
export function useInvalidateMessages() {
|
|
63
|
+
const queryClient = useQueryClient();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
invalidateChannel: (spaceId: string, channelId: string) => {
|
|
67
|
+
queryClient.invalidateQueries({
|
|
68
|
+
queryKey: queryKeys.messages.infinite(spaceId, channelId),
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
invalidateSpace: (spaceId: string) => {
|
|
72
|
+
queryClient.invalidateQueries({
|
|
73
|
+
queryKey: ['messages', 'infinite', spaceId],
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|