@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.
Files changed (51) hide show
  1. package/dist/index.d.mts +2414 -0
  2. package/dist/index.d.ts +2414 -0
  3. package/dist/index.js +2788 -0
  4. package/dist/index.mjs +2678 -0
  5. package/package.json +49 -0
  6. package/src/api/client.ts +86 -0
  7. package/src/api/endpoints.ts +87 -0
  8. package/src/api/errors.ts +179 -0
  9. package/src/api/index.ts +35 -0
  10. package/src/crypto/encryption-state.ts +249 -0
  11. package/src/crypto/index.ts +55 -0
  12. package/src/crypto/types.ts +307 -0
  13. package/src/crypto/wasm-provider.ts +298 -0
  14. package/src/hooks/index.ts +31 -0
  15. package/src/hooks/keys.ts +62 -0
  16. package/src/hooks/mutations/index.ts +15 -0
  17. package/src/hooks/mutations/useDeleteMessage.ts +67 -0
  18. package/src/hooks/mutations/useEditMessage.ts +87 -0
  19. package/src/hooks/mutations/useReaction.ts +163 -0
  20. package/src/hooks/mutations/useSendMessage.ts +131 -0
  21. package/src/hooks/useChannels.ts +49 -0
  22. package/src/hooks/useMessages.ts +77 -0
  23. package/src/hooks/useSpaces.ts +60 -0
  24. package/src/index.ts +32 -0
  25. package/src/signing/index.ts +10 -0
  26. package/src/signing/types.ts +83 -0
  27. package/src/signing/wasm-provider.ts +75 -0
  28. package/src/storage/adapter.ts +118 -0
  29. package/src/storage/index.ts +9 -0
  30. package/src/sync/index.ts +83 -0
  31. package/src/sync/service.test.ts +822 -0
  32. package/src/sync/service.ts +947 -0
  33. package/src/sync/types.ts +267 -0
  34. package/src/sync/utils.ts +588 -0
  35. package/src/transport/browser-websocket.ts +299 -0
  36. package/src/transport/index.ts +34 -0
  37. package/src/transport/rn-websocket.ts +321 -0
  38. package/src/transport/types.ts +56 -0
  39. package/src/transport/websocket.ts +212 -0
  40. package/src/types/bookmark.ts +29 -0
  41. package/src/types/conversation.ts +25 -0
  42. package/src/types/index.ts +57 -0
  43. package/src/types/message.ts +178 -0
  44. package/src/types/space.ts +75 -0
  45. package/src/types/user.ts +72 -0
  46. package/src/utils/encoding.ts +106 -0
  47. package/src/utils/formatting.ts +139 -0
  48. package/src/utils/index.ts +9 -0
  49. package/src/utils/logger.ts +141 -0
  50. package/src/utils/mentions.ts +135 -0
  51. 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
+ }