@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.
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/types/ChatifyContext.d.ts +11 -0
- package/dist/cjs/types/core/action.d.ts +73 -0
- package/dist/cjs/types/core/client.d.ts +35 -0
- package/dist/cjs/types/core/index.d.ts +14 -0
- package/dist/cjs/types/core/models.d.ts +64 -0
- package/dist/cjs/types/core/reducer.d.ts +11 -0
- package/dist/cjs/types/core/state.d.ts +25 -0
- package/dist/cjs/types/core/useAccounts.d.ts +1 -0
- package/dist/cjs/types/core/useAddChat.d.ts +2 -0
- package/dist/cjs/types/core/useAddMember.d.ts +2 -0
- package/dist/cjs/types/core/useAddMessage.d.ts +5 -0
- package/dist/cjs/types/core/useChat.d.ts +3 -0
- package/dist/cjs/types/core/useChatList.d.ts +1 -0
- package/dist/cjs/types/core/useLoadMore.d.ts +3 -0
- package/dist/cjs/types/core/useMessages.d.ts +15 -0
- package/dist/cjs/types/core/useMutation.d.ts +12 -0
- package/dist/cjs/types/core/useQuery.d.ts +7 -0
- package/dist/cjs/types/core/useReadMonitor.d.ts +1 -0
- package/dist/cjs/types/core/useRemoveMember.d.ts +2 -0
- package/dist/cjs/types/core/useSignalR.d.ts +1 -0
- package/dist/cjs/types/index.d.ts +3 -0
- package/dist/cjs/types/ui/AddChat.d.ts +1 -0
- package/dist/cjs/types/ui/Aside.d.ts +1 -0
- package/dist/cjs/types/ui/Chat.d.ts +4 -0
- package/dist/cjs/types/ui/ChatBody.d.ts +4 -0
- package/dist/cjs/types/ui/ChatHeader.d.ts +4 -0
- package/dist/cjs/types/ui/ChatSettings.d.ts +1 -0
- package/dist/cjs/types/ui/Chatify.d.ts +8 -0
- package/dist/cjs/types/ui/InputForm.d.ts +1 -0
- package/dist/cjs/types/ui/LoadMore.d.ts +4 -0
- package/dist/cjs/types/ui/Main.d.ts +1 -0
- package/dist/cjs/types/ui/Message.d.ts +5 -0
- package/dist/cjs/types/ui/MessageMd.d.ts +13 -0
- package/dist/cjs/types/ui/MessageSkeleton.d.ts +4 -0
- package/dist/cjs/types/ui/SendingMessage.d.ts +6 -0
- package/dist/cjs/types/ui/SystemMessage.d.ts +8 -0
- package/dist/cjs/types/ui/index.d.ts +2 -0
- package/dist/cjs/types/ui/localizer.d.ts +28 -0
- package/dist/cjs/types/util/index.d.ts +2 -0
- package/dist/cjs/types/util/uniqBy.d.ts +1 -0
- package/dist/cjs/types/util/uniqId.d.ts +1 -0
- package/dist/css/chatify.css +328 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types/ChatifyContext.d.ts +11 -0
- package/dist/esm/types/core/action.d.ts +73 -0
- package/dist/esm/types/core/client.d.ts +35 -0
- package/dist/esm/types/core/index.d.ts +14 -0
- package/dist/esm/types/core/models.d.ts +64 -0
- package/dist/esm/types/core/reducer.d.ts +11 -0
- package/dist/esm/types/core/state.d.ts +25 -0
- package/dist/esm/types/core/useAccounts.d.ts +1 -0
- package/dist/esm/types/core/useAddChat.d.ts +2 -0
- package/dist/esm/types/core/useAddMember.d.ts +2 -0
- package/dist/esm/types/core/useAddMessage.d.ts +5 -0
- package/dist/esm/types/core/useChat.d.ts +3 -0
- package/dist/esm/types/core/useChatList.d.ts +1 -0
- package/dist/esm/types/core/useLoadMore.d.ts +3 -0
- package/dist/esm/types/core/useMessages.d.ts +15 -0
- package/dist/esm/types/core/useMutation.d.ts +12 -0
- package/dist/esm/types/core/useQuery.d.ts +7 -0
- package/dist/esm/types/core/useReadMonitor.d.ts +1 -0
- package/dist/esm/types/core/useRemoveMember.d.ts +2 -0
- package/dist/esm/types/core/useSignalR.d.ts +1 -0
- package/dist/esm/types/index.d.ts +3 -0
- package/dist/esm/types/ui/AddChat.d.ts +1 -0
- package/dist/esm/types/ui/Aside.d.ts +1 -0
- package/dist/esm/types/ui/Chat.d.ts +4 -0
- package/dist/esm/types/ui/ChatBody.d.ts +4 -0
- package/dist/esm/types/ui/ChatHeader.d.ts +4 -0
- package/dist/esm/types/ui/ChatSettings.d.ts +1 -0
- package/dist/esm/types/ui/Chatify.d.ts +8 -0
- package/dist/esm/types/ui/InputForm.d.ts +1 -0
- package/dist/esm/types/ui/LoadMore.d.ts +4 -0
- package/dist/esm/types/ui/Main.d.ts +1 -0
- package/dist/esm/types/ui/Message.d.ts +5 -0
- package/dist/esm/types/ui/MessageMd.d.ts +13 -0
- package/dist/esm/types/ui/MessageSkeleton.d.ts +4 -0
- package/dist/esm/types/ui/SendingMessage.d.ts +6 -0
- package/dist/esm/types/ui/SystemMessage.d.ts +8 -0
- package/dist/esm/types/ui/index.d.ts +2 -0
- package/dist/esm/types/ui/localizer.d.ts +28 -0
- package/dist/esm/types/util/index.d.ts +2 -0
- package/dist/esm/types/util/uniqBy.d.ts +1 -0
- package/dist/esm/types/util/uniqId.d.ts +1 -0
- package/dist/index.d.ts +290 -0
- package/package.json +90 -0
- package/src/ChatifyContext.tsx +26 -0
- package/src/core/action.tsx +282 -0
- package/src/core/client.ts +137 -0
- package/src/core/index.ts +14 -0
- package/src/core/models.tsx +75 -0
- package/src/core/reducer.tsx +80 -0
- package/src/core/state.ts +38 -0
- package/src/core/useAccounts.tsx +11 -0
- package/src/core/useAddChat.tsx +12 -0
- package/src/core/useAddMember.tsx +12 -0
- package/src/core/useAddMessage.tsx +40 -0
- package/src/core/useChat.tsx +12 -0
- package/src/core/useChatList.tsx +10 -0
- package/src/core/useLoadMore.tsx +42 -0
- package/src/core/useMessages.tsx +23 -0
- package/src/core/useMutation.tsx +48 -0
- package/src/core/useQuery.tsx +45 -0
- package/src/core/useReadMonitor.tsx +31 -0
- package/src/core/useRemoveMember.tsx +12 -0
- package/src/core/useSignalR.tsx +51 -0
- package/src/index.ts +3 -0
- package/src/ui/AddChat.tsx +55 -0
- package/src/ui/Aside.tsx +67 -0
- package/src/ui/Chat.tsx +26 -0
- package/src/ui/ChatBody.tsx +43 -0
- package/src/ui/ChatHeader.tsx +47 -0
- package/src/ui/ChatSettings.tsx +59 -0
- package/src/ui/Chatify.tsx +47 -0
- package/src/ui/InputForm.tsx +48 -0
- package/src/ui/LoadMore.tsx +40 -0
- package/src/ui/Main.tsx +16 -0
- package/src/ui/Message.tsx +50 -0
- package/src/ui/MessageMd.tsx +47 -0
- package/src/ui/MessageSkeleton.tsx +19 -0
- package/src/ui/SendingMessage.tsx +14 -0
- package/src/ui/SystemMessage.tsx +26 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/localizer.tsx +58 -0
- package/src/ui/styles/aside.scss +94 -0
- package/src/ui/styles/index.scss +247 -0
- package/src/ui/styles/keyframes.scss +14 -0
- package/src/ui/styles/mixins.scss +20 -0
- package/src/ui/styles/variables.scss +10 -0
- package/src/util/index.ts +2 -0
- package/src/util/uniqBy.tsx +17 -0
- 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
|
+
}
|