@webinex/chatify 1.0.0-alpha00

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 (135) hide show
  1. package/dist/cjs/index.js +2 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/cjs/types/ChatifyContext.d.ts +11 -0
  4. package/dist/cjs/types/core/action.d.ts +73 -0
  5. package/dist/cjs/types/core/client.d.ts +35 -0
  6. package/dist/cjs/types/core/index.d.ts +14 -0
  7. package/dist/cjs/types/core/models.d.ts +64 -0
  8. package/dist/cjs/types/core/reducer.d.ts +11 -0
  9. package/dist/cjs/types/core/state.d.ts +25 -0
  10. package/dist/cjs/types/core/useAccounts.d.ts +1 -0
  11. package/dist/cjs/types/core/useAddChat.d.ts +2 -0
  12. package/dist/cjs/types/core/useAddMember.d.ts +2 -0
  13. package/dist/cjs/types/core/useAddMessage.d.ts +5 -0
  14. package/dist/cjs/types/core/useChat.d.ts +3 -0
  15. package/dist/cjs/types/core/useChatList.d.ts +1 -0
  16. package/dist/cjs/types/core/useLoadMore.d.ts +3 -0
  17. package/dist/cjs/types/core/useMessages.d.ts +15 -0
  18. package/dist/cjs/types/core/useMutation.d.ts +12 -0
  19. package/dist/cjs/types/core/useQuery.d.ts +7 -0
  20. package/dist/cjs/types/core/useReadMonitor.d.ts +1 -0
  21. package/dist/cjs/types/core/useRemoveMember.d.ts +2 -0
  22. package/dist/cjs/types/core/useSignalR.d.ts +1 -0
  23. package/dist/cjs/types/index.d.ts +3 -0
  24. package/dist/cjs/types/ui/AddChat.d.ts +1 -0
  25. package/dist/cjs/types/ui/Aside.d.ts +1 -0
  26. package/dist/cjs/types/ui/Chat.d.ts +4 -0
  27. package/dist/cjs/types/ui/ChatBody.d.ts +4 -0
  28. package/dist/cjs/types/ui/ChatHeader.d.ts +4 -0
  29. package/dist/cjs/types/ui/ChatSettings.d.ts +1 -0
  30. package/dist/cjs/types/ui/Chatify.d.ts +8 -0
  31. package/dist/cjs/types/ui/InputForm.d.ts +1 -0
  32. package/dist/cjs/types/ui/LoadMore.d.ts +4 -0
  33. package/dist/cjs/types/ui/Main.d.ts +1 -0
  34. package/dist/cjs/types/ui/Message.d.ts +5 -0
  35. package/dist/cjs/types/ui/MessageMd.d.ts +13 -0
  36. package/dist/cjs/types/ui/MessageSkeleton.d.ts +4 -0
  37. package/dist/cjs/types/ui/SendingMessage.d.ts +6 -0
  38. package/dist/cjs/types/ui/SystemMessage.d.ts +8 -0
  39. package/dist/cjs/types/ui/index.d.ts +2 -0
  40. package/dist/cjs/types/ui/localizer.d.ts +28 -0
  41. package/dist/cjs/types/util/index.d.ts +2 -0
  42. package/dist/cjs/types/util/uniqBy.d.ts +1 -0
  43. package/dist/cjs/types/util/uniqId.d.ts +1 -0
  44. package/dist/css/chatify.css +328 -0
  45. package/dist/esm/index.js +2 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/types/ChatifyContext.d.ts +11 -0
  48. package/dist/esm/types/core/action.d.ts +73 -0
  49. package/dist/esm/types/core/client.d.ts +35 -0
  50. package/dist/esm/types/core/index.d.ts +14 -0
  51. package/dist/esm/types/core/models.d.ts +64 -0
  52. package/dist/esm/types/core/reducer.d.ts +11 -0
  53. package/dist/esm/types/core/state.d.ts +25 -0
  54. package/dist/esm/types/core/useAccounts.d.ts +1 -0
  55. package/dist/esm/types/core/useAddChat.d.ts +2 -0
  56. package/dist/esm/types/core/useAddMember.d.ts +2 -0
  57. package/dist/esm/types/core/useAddMessage.d.ts +5 -0
  58. package/dist/esm/types/core/useChat.d.ts +3 -0
  59. package/dist/esm/types/core/useChatList.d.ts +1 -0
  60. package/dist/esm/types/core/useLoadMore.d.ts +3 -0
  61. package/dist/esm/types/core/useMessages.d.ts +15 -0
  62. package/dist/esm/types/core/useMutation.d.ts +12 -0
  63. package/dist/esm/types/core/useQuery.d.ts +7 -0
  64. package/dist/esm/types/core/useReadMonitor.d.ts +1 -0
  65. package/dist/esm/types/core/useRemoveMember.d.ts +2 -0
  66. package/dist/esm/types/core/useSignalR.d.ts +1 -0
  67. package/dist/esm/types/index.d.ts +3 -0
  68. package/dist/esm/types/ui/AddChat.d.ts +1 -0
  69. package/dist/esm/types/ui/Aside.d.ts +1 -0
  70. package/dist/esm/types/ui/Chat.d.ts +4 -0
  71. package/dist/esm/types/ui/ChatBody.d.ts +4 -0
  72. package/dist/esm/types/ui/ChatHeader.d.ts +4 -0
  73. package/dist/esm/types/ui/ChatSettings.d.ts +1 -0
  74. package/dist/esm/types/ui/Chatify.d.ts +8 -0
  75. package/dist/esm/types/ui/InputForm.d.ts +1 -0
  76. package/dist/esm/types/ui/LoadMore.d.ts +4 -0
  77. package/dist/esm/types/ui/Main.d.ts +1 -0
  78. package/dist/esm/types/ui/Message.d.ts +5 -0
  79. package/dist/esm/types/ui/MessageMd.d.ts +13 -0
  80. package/dist/esm/types/ui/MessageSkeleton.d.ts +4 -0
  81. package/dist/esm/types/ui/SendingMessage.d.ts +6 -0
  82. package/dist/esm/types/ui/SystemMessage.d.ts +8 -0
  83. package/dist/esm/types/ui/index.d.ts +2 -0
  84. package/dist/esm/types/ui/localizer.d.ts +28 -0
  85. package/dist/esm/types/util/index.d.ts +2 -0
  86. package/dist/esm/types/util/uniqBy.d.ts +1 -0
  87. package/dist/esm/types/util/uniqId.d.ts +1 -0
  88. package/dist/index.d.ts +290 -0
  89. package/package.json +90 -0
  90. package/src/ChatifyContext.tsx +26 -0
  91. package/src/core/action.tsx +282 -0
  92. package/src/core/client.ts +137 -0
  93. package/src/core/index.ts +14 -0
  94. package/src/core/models.tsx +75 -0
  95. package/src/core/reducer.tsx +80 -0
  96. package/src/core/state.ts +38 -0
  97. package/src/core/useAccounts.tsx +11 -0
  98. package/src/core/useAddChat.tsx +12 -0
  99. package/src/core/useAddMember.tsx +12 -0
  100. package/src/core/useAddMessage.tsx +40 -0
  101. package/src/core/useChat.tsx +12 -0
  102. package/src/core/useChatList.tsx +10 -0
  103. package/src/core/useLoadMore.tsx +42 -0
  104. package/src/core/useMessages.tsx +23 -0
  105. package/src/core/useMutation.tsx +48 -0
  106. package/src/core/useQuery.tsx +45 -0
  107. package/src/core/useReadMonitor.tsx +31 -0
  108. package/src/core/useRemoveMember.tsx +12 -0
  109. package/src/core/useSignalR.tsx +51 -0
  110. package/src/index.ts +3 -0
  111. package/src/ui/AddChat.tsx +55 -0
  112. package/src/ui/Aside.tsx +67 -0
  113. package/src/ui/Chat.tsx +26 -0
  114. package/src/ui/ChatBody.tsx +43 -0
  115. package/src/ui/ChatHeader.tsx +47 -0
  116. package/src/ui/ChatSettings.tsx +59 -0
  117. package/src/ui/Chatify.tsx +47 -0
  118. package/src/ui/InputForm.tsx +48 -0
  119. package/src/ui/LoadMore.tsx +40 -0
  120. package/src/ui/Main.tsx +16 -0
  121. package/src/ui/Message.tsx +50 -0
  122. package/src/ui/MessageMd.tsx +47 -0
  123. package/src/ui/MessageSkeleton.tsx +19 -0
  124. package/src/ui/SendingMessage.tsx +14 -0
  125. package/src/ui/SystemMessage.tsx +26 -0
  126. package/src/ui/index.ts +2 -0
  127. package/src/ui/localizer.tsx +58 -0
  128. package/src/ui/styles/aside.scss +94 -0
  129. package/src/ui/styles/index.scss +247 -0
  130. package/src/ui/styles/keyframes.scss +14 -0
  131. package/src/ui/styles/mixins.scss +20 -0
  132. package/src/ui/styles/variables.scss +10 -0
  133. package/src/util/index.ts +2 -0
  134. package/src/util/uniqBy.tsx +17 -0
  135. package/src/util/uniqId.tsx +3 -0
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@webinex/chatify",
3
+ "description": "Webinex Wispo",
4
+ "private": false,
5
+ "version": "1.0.0-alpha00",
6
+ "author": "Webinex Dev",
7
+ "license": "Apache-2.0",
8
+ "main": "dist/cjs/index.js",
9
+ "module": "dist/esm/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "type": "module",
12
+ "targets": {
13
+ "demo": {
14
+ "source": "./example/index.html",
15
+ "isLibrary": false
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "scripts": {
23
+ "build": "rimraf dist && rimraf add && rollup -c --environment ENV:production --failAfterWarnings",
24
+ "start": "rollup -c -w",
25
+ "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
26
+ "prettier:write": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
27
+ "prettier:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
28
+ "parcel": "parcel --target demo --dist-dir ./dist/demo",
29
+ "parcel:build": "rimraf ../Webinex.Chatify.Example/wwwroot && cross-env NODE_ENV=production parcel build --target demo --dist-dir ../Webinex.Chatify.Example/wwwroot"
30
+ },
31
+ "devDependencies": {
32
+ "@microsoft/signalr": "^8.0.0",
33
+ "@parcel/transformer-sass": "2.9.2",
34
+ "@rollup/plugin-commonjs": "^24.0.1",
35
+ "@rollup/plugin-eslint": "^9.0.3",
36
+ "@rollup/plugin-image": "^3.0.2",
37
+ "@rollup/plugin-node-resolve": "^15.0.2",
38
+ "@rollup/plugin-typescript": "^11.1.0",
39
+ "@types/react": "^18.0.33",
40
+ "@types/react-dom": "^18.0.11",
41
+ "antd": "^5.11.5",
42
+ "axios": "^1.6.2",
43
+ "buffer": "^5.5.0||^6.0.0",
44
+ "classnames": "^2.3.2",
45
+ "cross-env": "^7.0.3",
46
+ "dayjs": "^1.11.7",
47
+ "eslint": "^8.38.0",
48
+ "eslint-config-react-app": "^7.0.1",
49
+ "is-hotkey": "^0.2.0",
50
+ "parcel": "^2.10.3",
51
+ "postcss": "^8.4.21",
52
+ "prettier": "^2.8.7",
53
+ "process": "^0.11.10",
54
+ "react": "^18.2.0",
55
+ "react-dom": "^18.2.0",
56
+ "rimraf": "^5.0.5",
57
+ "rollup": "^3.20.2",
58
+ "rollup-plugin-copy": "^3.4.0",
59
+ "rollup-plugin-dts": "^5.3.0",
60
+ "rollup-plugin-peer-deps-external": "^2.2.4",
61
+ "rollup-plugin-postcss": "^4.0.2",
62
+ "rollup-plugin-scss": "^4.0.0",
63
+ "rollup-plugin-terser": "^7.0.2",
64
+ "rollup-plugin-visualizer": "^5.11.0",
65
+ "sass": "^1.66.1",
66
+ "shallow-equal": "^3.1.0",
67
+ "slate": "^0.94.1",
68
+ "slate-history": "^0.93.0",
69
+ "slate-react": "^0.98.3",
70
+ "tslib": "^2.6.2",
71
+ "typescript": "^5.2.2"
72
+ },
73
+ "peerDependencies": {
74
+ "@ant-design/icons": "^5.2.6",
75
+ "@ctrl/tinycolor": "^4.0.2",
76
+ "@microsoft/signalr": "^8.0.0",
77
+ "antd": ">=5",
78
+ "axios": ">=1",
79
+ "classnames": ">=2",
80
+ "dayjs": ">=1.8",
81
+ "is-hotkey": ">=0",
82
+ "react": ">=16",
83
+ "react-dom": ">=16",
84
+ "shallow-equal": ">=3",
85
+ "slate": "^0.94.1",
86
+ "slate-history": "^0.93.0",
87
+ "slate-react": "^0.98.3"
88
+ },
89
+ "dependencies": {}
90
+ }
@@ -0,0 +1,26 @@
1
+ import React, { PropsWithChildren, useContext } from 'react';
2
+ import { ChatifyClient } from './core';
3
+
4
+ const MeContext = React.createContext<string>(null!);
5
+ const ClientContext = React.createContext<ChatifyClient>(null!);
6
+
7
+ export function useClient() {
8
+ return useContext(ClientContext);
9
+ }
10
+
11
+ export function useMe() {
12
+ return useContext(MeContext);
13
+ }
14
+
15
+ export interface ChatifyContextValue {
16
+ me: string;
17
+ client: ChatifyClient;
18
+ }
19
+
20
+ export function ChatifyContext({ value, children }: PropsWithChildren<{ value: ChatifyContextValue }>) {
21
+ return (
22
+ <ClientContext.Provider value={value.client}>
23
+ <MeContext.Provider value={value.me}>{children}</MeContext.Provider>
24
+ </ClientContext.Provider>
25
+ );
26
+ }
@@ -0,0 +1,282 @@
1
+ import type { MutationState, QueryState, State } from './state';
2
+ import { Account, ChatListItem, Message, ReadEvent } from './models';
3
+ import { MessageState } from './useMessages';
4
+
5
+ function updateQuery<TData>(
6
+ state: State,
7
+ key: string,
8
+ mutation: (queryState: QueryState<TData> | undefined) => QueryState<TData> | undefined,
9
+ ) {
10
+ state = {
11
+ ...state,
12
+ query: {
13
+ ...state.query,
14
+ },
15
+ };
16
+
17
+ const result = mutation(state.query[key]);
18
+ if (result) {
19
+ state.query[key] = result;
20
+ }
21
+ return state;
22
+ }
23
+
24
+ function updateMutation(
25
+ state: State,
26
+ key: string,
27
+ mutation: (mutationState: MutationState | undefined) => MutationState | undefined,
28
+ ) {
29
+ state = {
30
+ ...state,
31
+ mutation: {
32
+ ...state.mutation,
33
+ },
34
+ };
35
+
36
+ const result = mutation(state.mutation[key]);
37
+ if (result) {
38
+ state.mutation[key] = result;
39
+ }
40
+ return state;
41
+ }
42
+
43
+ export const ACTIONS = {
44
+ query_fetch: (state: State, { key }: { key: string }): State => {
45
+ return updateQuery(state, key, (x) => ({ ...x, isFetching: true, uninitialized: false }));
46
+ },
47
+
48
+ query_resolve: (state: State, { key, data }: { key: string; data: any[] }): State => {
49
+ return updateQuery(state, key, (x) => ({ ...x!, isFetching: false, error: undefined, data }));
50
+ },
51
+
52
+ query_reject: (state: State, { key, error }: { key: string; error: any }): State => {
53
+ return updateQuery(state, key, (x) => ({ ...x!, isFetching: false, error }));
54
+ },
55
+
56
+ mutation_fetch: (state: State, { key }: { key: string }): State => {
57
+ return updateMutation(state, key, (x) => ({ ...x, isFetching: true }));
58
+ },
59
+
60
+ mutation_resolve: (state: State, { key }: { key: string }): State => {
61
+ return updateMutation(state, key, (x) => ({ ...x, isFetching: false, error: undefined }));
62
+ },
63
+
64
+ mutation_reject: (state: State, { key, error }: { key: string; error: any }): State => {
65
+ return updateMutation(state, key, (x) => ({ ...x, isFetching: false, error }));
66
+ },
67
+
68
+ update: (state: State, { mutation }: { mutation: (state: State) => State }): State => {
69
+ return mutation(state);
70
+ },
71
+
72
+ chat_open: (state: State, { chatId }: { chatId: string }): State => {
73
+ return { ...state, ui: { ...state.ui, view: 'chat', chatId } };
74
+ },
75
+
76
+ new_chat_open: (state: State): State => {
77
+ return { ...state, ui: { ...state.ui, view: 'new-chat', chatId: undefined } };
78
+ },
79
+
80
+ chat_settings_open: (state: State, { chatId }: { chatId: string }): State => {
81
+ return { ...state, ui: { ...state.ui, view: 'chat-settings', chatId } };
82
+ },
83
+
84
+ message_received: (
85
+ state: State,
86
+ { message, requestId }: { message: Message; requestId: string },
87
+ ): State => {
88
+ if (state.query['messages.' + message.chatId]?.data !== undefined) {
89
+ state = {
90
+ ...state,
91
+ query: {
92
+ ...state.query,
93
+ ['messages.' + message.chatId]: {
94
+ ...state.query['messages.' + message.chatId],
95
+ data: [{ type: 'message', ...message }, ...state.query['messages.' + message.chatId].data].filter(
96
+ (x: MessageState) => x.type !== 'sending' || x.requestId !== requestId,
97
+ ),
98
+ },
99
+ },
100
+ };
101
+ }
102
+
103
+ if (state.query['chat-list'].data !== undefined) {
104
+ state = {
105
+ ...state,
106
+ query: {
107
+ ...state.query,
108
+ 'chat-list': {
109
+ ...state.query['chat-list'],
110
+ data: state.query['chat-list'].data.map((x: ChatListItem) =>
111
+ x.id === message.chatId
112
+ ? { ...x, message, unreadCount: message.read ? x.unreadCount : x.unreadCount + 1 }
113
+ : x,
114
+ ),
115
+ },
116
+ },
117
+ };
118
+ }
119
+
120
+ return state;
121
+ },
122
+
123
+ chat_created: (state: State, { chat }: { chat: ChatListItem; requestId?: string }): State => {
124
+ if (state.query['chat-list']?.data === undefined) {
125
+ return state;
126
+ }
127
+
128
+ return {
129
+ ...state,
130
+ query: {
131
+ ...state.query,
132
+ 'chat-list': {
133
+ ...state.query['chat-list'],
134
+ data: [chat, ...state.query['chat-list'].data!],
135
+ },
136
+ },
137
+ };
138
+ },
139
+
140
+ read: (state: State, { id, timestamp }: { id: string; timestamp: number }): State => {
141
+ return {
142
+ ...state,
143
+ read: {
144
+ ...state.read,
145
+ queued: [...state.read.queued, id],
146
+ timestamp,
147
+ },
148
+ };
149
+ },
150
+
151
+ read_send: (state: State, { ids }: { ids: string[] }): State => {
152
+ return {
153
+ ...state,
154
+ read: {
155
+ ...state.read,
156
+ queued: state.read.queued.filter((x) => !ids.includes(x)),
157
+ sent: [...state.read.sent, ...ids],
158
+ },
159
+ };
160
+ },
161
+
162
+ read_reject: (state: State, { ids, timestamp }: { ids: string[]; timestamp: number }): State => {
163
+ return {
164
+ ...state,
165
+ read: {
166
+ ...state.read,
167
+ queued: [...state.read.queued, ...ids],
168
+ sent: state.read.sent.filter((x) => !ids.includes(x)),
169
+ timestamp,
170
+ },
171
+ };
172
+ },
173
+
174
+ read_received: (state: State, { events }: { events: ReadEvent[] }): State => {
175
+ const ids = events.map((x) => x.messageId);
176
+
177
+ state = {
178
+ ...state,
179
+ query: {
180
+ ...state.query,
181
+ },
182
+ read: {
183
+ ...state.read,
184
+ sent: state.read.sent.filter((x) => !ids.includes(x)),
185
+ },
186
+ };
187
+
188
+ Object.keys(state.query)
189
+ .filter((x) => x.startsWith('messages.'))
190
+ .forEach((key) => {
191
+ state.query[key] = { ...state.query[key] };
192
+ const queryState: QueryState<MessageState[]> = state.query[key];
193
+
194
+ queryState.data = queryState.data?.map((x) => {
195
+ if (x.type === 'message' && ids.includes(x.id)) {
196
+ return { ...x, read: true };
197
+ }
198
+
199
+ return x;
200
+ });
201
+ });
202
+
203
+ state.query['chat-list'] = { ...state.query['chat-list'] };
204
+ const chatListState: QueryState<ChatListItem[]> = state.query['chat-list'];
205
+ chatListState.data = chatListState.data?.map((x) =>
206
+ ids.includes(x.message.id) ? { ...x, message: { ...x.message, read: true } } : x,
207
+ );
208
+
209
+ chatListState.data = chatListState.data?.map((chat) => ({
210
+ ...chat,
211
+ unreadCount: chat.unreadCount - events.filter((x) => x.chatId === chat.id).length,
212
+ }));
213
+
214
+ return state;
215
+ },
216
+
217
+ member_added: (state: State, { chatId, account }: { chatId: string; account: Account }): State => {
218
+ state = {
219
+ ...state,
220
+ query: {
221
+ ...state.query,
222
+ },
223
+ };
224
+
225
+ if (state.query['chat.' + chatId]?.data !== undefined) {
226
+ state.query['chat.' + chatId] = {
227
+ ...state.query['chat.' + chatId],
228
+ data: { ...state.query['chat.' + chatId].data },
229
+ };
230
+ state.query['chat.' + chatId].data.members = [...state.query['chat.' + chatId].data.members, account];
231
+ }
232
+
233
+ return state;
234
+ },
235
+
236
+ member_removed: (
237
+ state: State,
238
+ {
239
+ chatId,
240
+ accountId,
241
+ deleteHistory,
242
+ me,
243
+ }: { chatId: string; accountId: string; deleteHistory: boolean; me: string },
244
+ ): State => {
245
+ state = {
246
+ ...state,
247
+ query: {
248
+ ...state.query,
249
+ },
250
+ };
251
+
252
+ if (state.query['chat.' + chatId]?.data !== undefined) {
253
+ state.query['chat.' + chatId] = {
254
+ ...state.query['chat.' + chatId],
255
+ data: { ...state.query['chat.' + chatId].data },
256
+ };
257
+ state.query['chat.' + chatId].data.members = state.query['chat.' + chatId].data.members.filter(
258
+ (x: Account) => x.id !== accountId,
259
+ );
260
+ }
261
+
262
+ if (accountId === me && deleteHistory && state.query['chat-list']?.data) {
263
+ state.query['chat-list'] = { ...state.query['chat-list'] };
264
+ state.query['chat-list'].data = state.query['chat-list'].data.filter(
265
+ (x: ChatListItem) => x.id !== chatId,
266
+ );
267
+ }
268
+
269
+ if (accountId === me && deleteHistory && state.query['chat-list']?.data && state.ui.chatId === chatId) {
270
+ state.ui = { ...state.ui, chatId: state.query['chat-list']?.data.at(0)?.id };
271
+ }
272
+
273
+ return state;
274
+ },
275
+ };
276
+
277
+ export type Action = {
278
+ [K in keyof typeof ACTIONS]: {
279
+ type: K;
280
+ data: Parameters<(typeof ACTIONS)[K]>[1];
281
+ };
282
+ }[keyof typeof ACTIONS];
@@ -0,0 +1,137 @@
1
+ import { AxiosInstance } from 'axios';
2
+ import * as SignalR from '@microsoft/signalr';
3
+ import {
4
+ Account,
5
+ AddChatRequest,
6
+ AddMemberRequest,
7
+ Chat,
8
+ ChatListItem,
9
+ Message,
10
+ ReadRequest,
11
+ RemoveMemberRequest,
12
+ SendMessageRequest,
13
+ } from './models';
14
+
15
+ export interface ChatifySignalRConfig {
16
+ hubUri?: string;
17
+ accessTokenFactory?: () => Promise<string>;
18
+ headersFactory?: () => Promise<Record<string, string>>;
19
+ }
20
+
21
+ const DEFAULT_SIGNALR_CONFIG: ChatifySignalRConfig = {
22
+ hubUri: '/api/chatify/hub',
23
+ headersFactory: () => Promise.resolve({}),
24
+ };
25
+
26
+ export interface ChatifyClientConfig {
27
+ axios: AxiosInstance;
28
+ signalR: ChatifySignalRConfig;
29
+ }
30
+
31
+ const METHODS = [
32
+ 'chatify://new-message',
33
+ 'chatify://chat-created',
34
+ 'chatify://read',
35
+ 'chatify://member-added',
36
+ 'chatify://member-removed',
37
+ ] as const;
38
+
39
+ export class ChatifyClient {
40
+ private _config: ChatifyClientConfig;
41
+ private _connection: SignalR.HubConnection = null!;
42
+ private _subscribers: Record<string, ((args: any[]) => void)[]> = {};
43
+ private _reconnectSubscribers: Array<() => void> = [];
44
+
45
+ constructor(config: ChatifyClientConfig) {
46
+ this._config = {
47
+ ...config,
48
+ signalR: {
49
+ ...DEFAULT_SIGNALR_CONFIG,
50
+ ...config.signalR,
51
+ },
52
+ };
53
+ }
54
+
55
+ public connect = async () => {
56
+ const headers = await this._config.signalR!.headersFactory!();
57
+
58
+ this._connection = new SignalR.HubConnectionBuilder()
59
+ .withUrl(this._config.signalR!.hubUri!, {
60
+ accessTokenFactory: this._config.signalR!.accessTokenFactory
61
+ ? this._config.signalR!.accessTokenFactory
62
+ : undefined,
63
+ headers,
64
+ })
65
+ .withAutomaticReconnect()
66
+ .build();
67
+
68
+ METHODS.forEach((x) => {
69
+ this._connection.on(x, (...args) => {
70
+ this._subscribers[x]?.forEach((s) => s(args));
71
+ });
72
+ });
73
+
74
+ this._connection.onreconnected(() => this._reconnectSubscribers.forEach((subscriber) => subscriber()));
75
+
76
+ await this._connection.start();
77
+ };
78
+
79
+ public subscribe = <T extends any[]>(method: string, subscriber: (args: T) => void) => {
80
+ this._subscribers[method] = this._subscribers[method] ?? [];
81
+ this._subscribers[method].push(subscriber as any);
82
+ return () => this._subscribers[method].filter((x) => x !== subscriber);
83
+ };
84
+
85
+ public subscribeReconnect = (subscriber: () => any) => {
86
+ this._reconnectSubscribers.push(subscriber);
87
+ return () => this._reconnectSubscribers.filter((x) => x !== subscriber);
88
+ };
89
+
90
+ private get axios() {
91
+ return this._config.axios;
92
+ }
93
+
94
+ public accounts = async () => {
95
+ const { data } = await this.axios.get<Account[]>('account');
96
+ return data;
97
+ };
98
+
99
+ public chats = async () => {
100
+ const { data } = await this.axios.get<ChatListItem[]>('chat');
101
+ return data;
102
+ };
103
+
104
+ public chat = async (id: string) => {
105
+ const { data } = await this.axios.get<Chat>(`chat/${id}`);
106
+ return data;
107
+ };
108
+
109
+ public addChat = async (args: AddChatRequest) => {
110
+ const { data } = await this.axios.post<string>(`chat`, args);
111
+ return data;
112
+ };
113
+
114
+ public messages = async (args: { chatId: string; skip?: number; take?: number }) => {
115
+ const { chatId, skip = 0, take = 20 } = args;
116
+ const pagingRule = encodeURIComponent(JSON.stringify({ skip, take }));
117
+ const { data } = await this.axios.get<Message[]>(`chat/${chatId}/message?pagingRule=${pagingRule}`);
118
+ return data;
119
+ };
120
+
121
+ public send = async (request: SendMessageRequest) => {
122
+ const { chatId, ...rest } = request;
123
+ await this.axios.post(`chat/${chatId}/message`, rest);
124
+ };
125
+
126
+ public read = async (request: ReadRequest) => {
127
+ await this.axios.put(`chat/message/read`, request);
128
+ };
129
+
130
+ public removeMember = async (request: RemoveMemberRequest) => {
131
+ await this.axios.delete(`chat/${request.chatId}/member`, { data: request });
132
+ };
133
+
134
+ public addMember = async (request: AddMemberRequest) => {
135
+ await this.axios.post(`chat/${request.chatId}/member`, request);
136
+ };
137
+ }
@@ -0,0 +1,14 @@
1
+ export * from './models';
2
+ export * from './client';
3
+ export * from './action';
4
+ export * from './reducer';
5
+ export * from './useChat';
6
+ export * from './useChatList';
7
+ export * from './useLoadMore';
8
+ export * from './useMessages';
9
+ export * from './useSignalR';
10
+ export * from './useAccounts';
11
+ export * from './useAddChat';
12
+ export * from './useReadMonitor';
13
+ export * from './useAddMember';
14
+ export * from './useRemoveMember';
@@ -0,0 +1,75 @@
1
+ export interface File {
2
+ name: string;
3
+ bytes: number;
4
+ ref: string;
5
+ }
6
+
7
+ export interface Account {
8
+ id: string;
9
+ name: string;
10
+ avatar: string;
11
+ }
12
+
13
+ export interface Message {
14
+ id: string;
15
+ chatId: string;
16
+ text: string;
17
+ files: File[];
18
+ sentAt: string;
19
+ sentBy: Account;
20
+ read: boolean;
21
+ requestId?: string;
22
+ }
23
+
24
+ export interface Chat {
25
+ id: string;
26
+ name: string;
27
+ members: Account[];
28
+ }
29
+
30
+ export interface ChatListItem {
31
+ id: string;
32
+ name: string;
33
+ message: Message;
34
+ unreadCount: number;
35
+ }
36
+
37
+ export interface SendMessageRequest {
38
+ chatId: string;
39
+ text: string;
40
+ files: File[];
41
+ requestId: string;
42
+ }
43
+
44
+ export interface MessageContent {
45
+ text: string;
46
+ files: File[];
47
+ }
48
+
49
+ export interface AddChatRequest {
50
+ requestId: string;
51
+ name: string;
52
+ message?: MessageContent;
53
+ members: string[];
54
+ }
55
+
56
+ export interface ReadRequest {
57
+ ids: string[];
58
+ }
59
+
60
+ export interface ReadEvent {
61
+ messageId: string;
62
+ chatId: string;
63
+ }
64
+
65
+ export interface RemoveMemberRequest {
66
+ chatId: string;
67
+ accountId: string;
68
+ deleteHistory: boolean;
69
+ }
70
+
71
+ export interface AddMemberRequest {
72
+ chatId: string;
73
+ accountId: string;
74
+ withHistory: boolean;
75
+ }
@@ -0,0 +1,80 @@
1
+ import React, { PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
+ import { ACTIONS, Action } from './action';
3
+ import { INITIAL_STATE, State } from './state';
4
+ import { shallowEqual } from 'shallow-equal';
5
+
6
+ function reducer(state: State, action: Action) {
7
+ const result = ACTIONS[action.type](state, action.data as any);
8
+ console.debug('[Chatify]: ', action, result);
9
+ return result;
10
+ }
11
+
12
+ export type Reducer = ReturnType<typeof useReducer>;
13
+
14
+ type Subscriber = (state: State) => void;
15
+
16
+ export function useReducer() {
17
+ const state = useRef(INITIAL_STATE);
18
+ const subscribers = useRef<Subscriber[]>([]);
19
+
20
+ const dispatch = useCallback((action: Action) => {
21
+ const newState = reducer(state.current, action);
22
+ if (shallowEqual(state.current, newState)) {
23
+ return;
24
+ }
25
+
26
+ state.current = newState;
27
+ subscribers.current.forEach((s) => s(newState));
28
+ }, []);
29
+
30
+ const subscribe = useCallback((subscriber: Subscriber) => {
31
+ subscribers.current.push(subscriber);
32
+ return () => {
33
+ subscribers.current = subscribers.current.filter((x) => x !== subscriber);
34
+ };
35
+ }, []);
36
+
37
+ return [state, dispatch, subscribe] as const;
38
+ }
39
+
40
+ const ReducerContext = React.createContext<Reducer>(null!);
41
+ export function ReducerProvider({ children }: PropsWithChildren<{}>) {
42
+ const reducer = useReducer();
43
+ return <ReducerContext.Provider value={reducer}>{children}</ReducerContext.Provider>;
44
+ }
45
+
46
+ export function useDispatch() {
47
+ return useContext(ReducerContext)[1];
48
+ }
49
+
50
+ export function useStateRef() {
51
+ const [reducerState] = useContext(ReducerContext);
52
+ return reducerState;
53
+ }
54
+
55
+ export function useSelect<TResult>(
56
+ select: (state: State) => TResult,
57
+
58
+ // TODO: make deps work
59
+ deps: any[],
60
+ ): TResult {
61
+ const [reducerState, , subscribe] = useContext(ReducerContext);
62
+ const prev = useRef(select(reducerState.current));
63
+ const [state, setState] = useState(prev.current);
64
+
65
+ useEffect(() => {
66
+ return subscribe((state) => {
67
+ const newValue = select(state);
68
+ if (shallowEqual<any>(newValue, prev.current)) {
69
+ return;
70
+ }
71
+
72
+ prev.current = newValue;
73
+ setState(newValue);
74
+ });
75
+
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, []);
78
+
79
+ return state;
80
+ }