@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface UIState {
|
|
2
|
+
view?: 'chat' | 'new-chat' | 'chat-settings';
|
|
3
|
+
chatId?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface QueryState<TData> {
|
|
7
|
+
uninitialized: boolean;
|
|
8
|
+
isFetching: boolean;
|
|
9
|
+
error?: any;
|
|
10
|
+
data?: TData;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MutationState {
|
|
14
|
+
isFetching: boolean;
|
|
15
|
+
error?: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface State {
|
|
19
|
+
ui: UIState;
|
|
20
|
+
query: Record<string, QueryState<any>>;
|
|
21
|
+
mutation: Record<string, MutationState>;
|
|
22
|
+
read: {
|
|
23
|
+
queued: string[];
|
|
24
|
+
sent: string[];
|
|
25
|
+
timestamp: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const INITIAL_STATE: State = {
|
|
30
|
+
ui: {},
|
|
31
|
+
query: {},
|
|
32
|
+
mutation: {},
|
|
33
|
+
read: {
|
|
34
|
+
queued: [],
|
|
35
|
+
sent: [],
|
|
36
|
+
timestamp: 0,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { AddChatRequest } from './models';
|
|
3
|
+
import { useMutation } from './useMutation';
|
|
4
|
+
|
|
5
|
+
export function useAddChat() {
|
|
6
|
+
const client = useClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
key: 'add-chat',
|
|
10
|
+
queryFn: (args: AddChatRequest) => client.addChat(args),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { AddMemberRequest } from './models';
|
|
3
|
+
import { useMutation } from './useMutation';
|
|
4
|
+
|
|
5
|
+
export function useAddMember() {
|
|
6
|
+
const client = useClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
key: 'add-member',
|
|
10
|
+
queryFn: (args: AddMemberRequest) => client.addMember(args),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { SendMessageRequest } from './models';
|
|
3
|
+
import { QueryState } from './state';
|
|
4
|
+
import { MessageState } from './useMessages';
|
|
5
|
+
import { useMutation } from './useMutation';
|
|
6
|
+
|
|
7
|
+
export interface UseAddMessageArgs {
|
|
8
|
+
chatId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useAddMessage(args: UseAddMessageArgs) {
|
|
12
|
+
const { chatId } = args;
|
|
13
|
+
const client = useClient();
|
|
14
|
+
|
|
15
|
+
return useMutation<unknown, Omit<SendMessageRequest, 'chatId'>>({
|
|
16
|
+
key: 'add.' + chatId,
|
|
17
|
+
queryFn: (args) => client.send({ chatId, ...args }),
|
|
18
|
+
onQueryStarted: async (args, { updateState }) => {
|
|
19
|
+
updateState((state) => {
|
|
20
|
+
state = {
|
|
21
|
+
...state,
|
|
22
|
+
query: {
|
|
23
|
+
...state.query,
|
|
24
|
+
['messages.' + chatId]: {
|
|
25
|
+
...state.query['messages.' + chatId],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const queryState: QueryState<MessageState[]> = state.query['messages.' + chatId];
|
|
31
|
+
queryState.data = [
|
|
32
|
+
{ type: 'sending', text: args.text, requestId: args.requestId, files: args.files },
|
|
33
|
+
...queryState.data!,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
return state;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { useQuery } from './useQuery';
|
|
3
|
+
|
|
4
|
+
export function useChat(args: { chatId: string }) {
|
|
5
|
+
const { chatId } = args;
|
|
6
|
+
const client = useClient();
|
|
7
|
+
|
|
8
|
+
return useQuery({
|
|
9
|
+
key: 'chat.' + chatId,
|
|
10
|
+
queryFn: () => client.chat(chatId),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { useMutation } from './useMutation';
|
|
3
|
+
import { MessageState } from './useMessages';
|
|
4
|
+
import { useStateRef } from './reducer';
|
|
5
|
+
import { QueryState } from './state';
|
|
6
|
+
|
|
7
|
+
export function useLoadMore({ chatId }: { chatId: string }) {
|
|
8
|
+
const state = useStateRef();
|
|
9
|
+
const client = useClient();
|
|
10
|
+
|
|
11
|
+
return useMutation({
|
|
12
|
+
key: 'more.' + chatId,
|
|
13
|
+
queryFn: () =>
|
|
14
|
+
client.messages({
|
|
15
|
+
chatId,
|
|
16
|
+
skip: state.current.query['messages.' + chatId].data.filter((x: MessageState) => x.type === 'message')
|
|
17
|
+
.length,
|
|
18
|
+
take: 15,
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
onQueryStarted: async (_, { promise, updateState }) => {
|
|
22
|
+
const result = await promise;
|
|
23
|
+
updateState((state) => {
|
|
24
|
+
state = { ...state };
|
|
25
|
+
state.query = { ...state.query };
|
|
26
|
+
state.query['messages.' + chatId] = { ...state.query['messages.' + chatId] };
|
|
27
|
+
const value: QueryState<MessageState[]> = state.query['messages.' + chatId];
|
|
28
|
+
value.data = value.data?.filter((x) => x.type !== 'more');
|
|
29
|
+
value.data = [
|
|
30
|
+
...value.data!,
|
|
31
|
+
...result.map((x) => ({ type: 'message' as const, ...x, proceed: true })),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
if (result.length === 15) {
|
|
35
|
+
value.data = [...value.data!, { type: 'more' }];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return state;
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { File, Message } from './models';
|
|
3
|
+
import { QueryState } from './state';
|
|
4
|
+
import { useQuery } from './useQuery';
|
|
5
|
+
|
|
6
|
+
export type MessageState =
|
|
7
|
+
| ({ type: 'message' } & Message)
|
|
8
|
+
| { type: 'more' }
|
|
9
|
+
| { type: 'sending'; text: string; files: File[]; requestId: string };
|
|
10
|
+
|
|
11
|
+
export function useMessages(args: { chatId: string }): QueryState<MessageState[]> {
|
|
12
|
+
const { chatId } = args;
|
|
13
|
+
const client = useClient();
|
|
14
|
+
|
|
15
|
+
return useQuery({
|
|
16
|
+
key: 'messages.' + chatId,
|
|
17
|
+
queryFn: () =>
|
|
18
|
+
client.messages({ chatId, skip: 0, take: 15 }).then((x) => {
|
|
19
|
+
const result = x.map((message) => ({ type: 'message' as const, ...message, proceed: true }));
|
|
20
|
+
return x.length === 15 ? [...result, { type: 'more' as const }] : result;
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useDispatch, useSelect, useStateRef } from './reducer';
|
|
3
|
+
import { MutationState, State } from './state';
|
|
4
|
+
|
|
5
|
+
export interface MutationApi<TResult> {
|
|
6
|
+
promise: Promise<TResult>;
|
|
7
|
+
state: State;
|
|
8
|
+
updateState: (mutation: (state: State) => State) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseMutationArgs<TResult = unknown, TArgs = void> {
|
|
12
|
+
key: string;
|
|
13
|
+
queryFn: (args: TArgs) => Promise<TResult>;
|
|
14
|
+
onQueryStarted?: (args: TArgs, api: MutationApi<TResult>) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const INITIAL_MUTATION_STATE: MutationState = { isFetching: false, error: undefined };
|
|
18
|
+
|
|
19
|
+
export function useMutation<TResult = unknown, TArgs = void>(args: UseMutationArgs<TResult, TArgs>) {
|
|
20
|
+
const { key, queryFn, onQueryStarted } = args;
|
|
21
|
+
const dispatch = useDispatch();
|
|
22
|
+
const state = useStateRef();
|
|
23
|
+
const mutationState = useSelect((x) => x.mutation[args.key], []);
|
|
24
|
+
|
|
25
|
+
const invoke = useCallback((args: TArgs) => {
|
|
26
|
+
dispatch({ type: 'mutation_fetch', data: { key } });
|
|
27
|
+
const promise = queryFn(args);
|
|
28
|
+
|
|
29
|
+
promise
|
|
30
|
+
.then(() => {
|
|
31
|
+
dispatch({ type: 'mutation_resolve', data: { key } });
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => dispatch({ type: 'mutation_reject', data: { key, error } }));
|
|
34
|
+
|
|
35
|
+
onQueryStarted &&
|
|
36
|
+
onQueryStarted(args, {
|
|
37
|
+
promise,
|
|
38
|
+
state: state.current,
|
|
39
|
+
updateState: (mutation: (state: State) => State) => {
|
|
40
|
+
dispatch({ type: 'update', data: { mutation } });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return [invoke, mutationState ?? INITIAL_MUTATION_STATE] as const;
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDispatch, useSelect, useStateRef } from './reducer';
|
|
3
|
+
import { QueryState, State } from './state';
|
|
4
|
+
|
|
5
|
+
interface UseQueryArgs<TData> {
|
|
6
|
+
key: string;
|
|
7
|
+
queryFn: () => Promise<TData>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const INITIAL_QUERY_STATE: QueryState<any> = {
|
|
11
|
+
uninitialized: false,
|
|
12
|
+
isFetching: true,
|
|
13
|
+
data: undefined,
|
|
14
|
+
error: undefined,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function select<TData>(state: State, key: string): QueryState<TData> | undefined {
|
|
18
|
+
return (state.query as any)[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useQueryState<TData>(key: string): QueryState<TData> | undefined {
|
|
22
|
+
return useSelect((x) => select<TData>(x, key), []);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useQuery<TData>(args: UseQueryArgs<TData>): QueryState<TData> {
|
|
26
|
+
const { key, queryFn } = args;
|
|
27
|
+
const state = useStateRef();
|
|
28
|
+
const dispatch = useDispatch();
|
|
29
|
+
const queryState = useQueryState<TData>(args.key);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (select(state.current, key)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
dispatch({ type: 'query_fetch', data: { key } });
|
|
37
|
+
queryFn()
|
|
38
|
+
.then((data) => dispatch({ type: 'query_resolve', data: { key, data: data as any } }))
|
|
39
|
+
.catch((error) => dispatch({ type: 'query_reject', data: { key, error } }));
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return queryState ?? INITIAL_QUERY_STATE;
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useClient } from '../ChatifyContext';
|
|
3
|
+
import { useDispatch, useSelect } from './reducer';
|
|
4
|
+
|
|
5
|
+
export function useReadMonitor() {
|
|
6
|
+
const client = useClient();
|
|
7
|
+
const [queued, timestamp] = useSelect((x) => [x.read.queued, x.read.timestamp], []);
|
|
8
|
+
const dispatch = useDispatch();
|
|
9
|
+
const timer = useRef<NodeJS.Timeout | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (queued.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (timer.current) {
|
|
17
|
+
clearTimeout(timer.current);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
timer.current = setTimeout(() => {
|
|
21
|
+
dispatch({ type: 'read_send', data: { ids: queued } });
|
|
22
|
+
client
|
|
23
|
+
.read({ ids: queued })
|
|
24
|
+
.catch(() =>
|
|
25
|
+
dispatch({ type: 'read_reject', data: { ids: queued, timestamp: new Date().getTime() } }),
|
|
26
|
+
);
|
|
27
|
+
}, 100);
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [timestamp]);
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useClient } from '../ChatifyContext';
|
|
2
|
+
import { RemoveMemberRequest } from './models';
|
|
3
|
+
import { useMutation } from './useMutation';
|
|
4
|
+
|
|
5
|
+
export function useRemoveMember() {
|
|
6
|
+
const client = useClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
key: 'remove-member',
|
|
10
|
+
queryFn: (args: RemoveMemberRequest) => client.removeMember(args),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDispatch } from './reducer';
|
|
3
|
+
import { useClient, useMe } from '../ChatifyContext';
|
|
4
|
+
import { Account, ChatListItem, Message, ReadEvent } from './models';
|
|
5
|
+
|
|
6
|
+
export function useSignalR() {
|
|
7
|
+
const client = useClient();
|
|
8
|
+
const dispatch = useDispatch();
|
|
9
|
+
const me = useMe();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
let unsubscribes: Array<() => void> = [];
|
|
13
|
+
|
|
14
|
+
client.connect().then(() => {
|
|
15
|
+
unsubscribes.push(
|
|
16
|
+
client.subscribe('chatify://new-message', ([message, requestId]: [Message, string]) => {
|
|
17
|
+
dispatch({ type: 'message_received', data: { message, requestId } });
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
unsubscribes.push(
|
|
21
|
+
client.subscribe('chatify://chat-created', ([chat, requestId]: [ChatListItem, string]) => {
|
|
22
|
+
dispatch({ type: 'chat_created', data: { chat, requestId } });
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
unsubscribes.push(
|
|
26
|
+
client.subscribe('chatify://read', ([events]: [ReadEvent[]]) => {
|
|
27
|
+
dispatch({ type: 'read_received', data: { events } });
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
unsubscribes.push(
|
|
31
|
+
client.subscribe('chatify://member-added', ([chatId, account]: [string, Account]) => {
|
|
32
|
+
dispatch({ type: 'member_added', data: { chatId, account } });
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
unsubscribes.push(
|
|
36
|
+
client.subscribe(
|
|
37
|
+
'chatify://member-removed',
|
|
38
|
+
([chatId, accountId, deleteHistory]: [string, string, boolean]) => {
|
|
39
|
+
dispatch({ type: 'member_removed', data: { chatId, accountId, deleteHistory, me } });
|
|
40
|
+
},
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
unsubscribes.forEach((x) => x());
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, []);
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Button, Form, Input, Select } from 'antd';
|
|
2
|
+
import { useAccounts, useAddChat } from '../core';
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { uniqId } from '../util';
|
|
5
|
+
import { useLocalizer } from './localizer';
|
|
6
|
+
|
|
7
|
+
type FormValue = {
|
|
8
|
+
name: string;
|
|
9
|
+
members: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function AddChat() {
|
|
13
|
+
const [add, { isFetching: adding }] = useAddChat();
|
|
14
|
+
const { data: accounts, isFetching } = useAccounts();
|
|
15
|
+
const localizer = useLocalizer();
|
|
16
|
+
const options = useMemo(() => accounts?.map((x) => ({ value: x.id, label: x.name })), [accounts]);
|
|
17
|
+
|
|
18
|
+
const onSubmit = useCallback(
|
|
19
|
+
(value: FormValue) =>
|
|
20
|
+
add({
|
|
21
|
+
name: value.name,
|
|
22
|
+
members: value.members,
|
|
23
|
+
requestId: uniqId(),
|
|
24
|
+
}),
|
|
25
|
+
[add],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="wxchtf-add-chat">
|
|
30
|
+
<h3>{localizer.addChat.title()}</h3>
|
|
31
|
+
<Form onFinish={onSubmit} layout="vertical" disabled={adding}>
|
|
32
|
+
<Form.Item
|
|
33
|
+
name="name"
|
|
34
|
+
label={localizer.addChat.name.label()}
|
|
35
|
+
rules={[
|
|
36
|
+
{ required: true, message: localizer.addChat.name.required() },
|
|
37
|
+
{ max: 250, message: localizer.addChat.name.max250() },
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
<Input />
|
|
41
|
+
</Form.Item>
|
|
42
|
+
<Form.Item
|
|
43
|
+
name="members"
|
|
44
|
+
label={localizer.addChat.members.label()}
|
|
45
|
+
rules={[{ required: true, message: localizer.addChat.members.required() }]}
|
|
46
|
+
>
|
|
47
|
+
<Select mode="multiple" loading={isFetching} options={options} />
|
|
48
|
+
</Form.Item>
|
|
49
|
+
<Button loading={adding} type="primary" block htmlType="submit">
|
|
50
|
+
{localizer.addChat.submitBtn()}
|
|
51
|
+
</Button>
|
|
52
|
+
</Form>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
package/src/ui/Aside.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Avatar, Menu, MenuProps } from 'antd';
|
|
2
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
|
4
|
+
import { useChatList, useDispatch, useSelect, ChatListItem } from '../core';
|
|
5
|
+
import { useLocalizer } from './localizer';
|
|
6
|
+
import { formatSystemMessage } from './SystemMessage';
|
|
7
|
+
|
|
8
|
+
function Chat(props: { chat: ChatListItem }) {
|
|
9
|
+
const { chat } = props;
|
|
10
|
+
const { name, message, unreadCount } = chat;
|
|
11
|
+
const localizer = useLocalizer();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className={'wxchtf-chat' + (!message.read ? ' --unread' : '')}>
|
|
15
|
+
<div className="wxchtf-chat-name">{name}</div>
|
|
16
|
+
{unreadCount > 0 && <div className="wxchtf-unread-count">{unreadCount}</div>}
|
|
17
|
+
<div className="wxchtf-chat-last">
|
|
18
|
+
{message.sentBy.id !== 'chatify::system' && (
|
|
19
|
+
<div className="wxchtf-chat-last-author">
|
|
20
|
+
<Avatar src={message.sentBy.avatar} /> {message.sentBy.name}
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
<div className="wxchtf-chat-last-message">
|
|
24
|
+
{message.sentBy.id === 'chatify::system'
|
|
25
|
+
? formatSystemMessage(localizer, message.text)
|
|
26
|
+
: message.text}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Aside() {
|
|
34
|
+
const activeChatId = useSelect((x) => x.ui.chatId, []);
|
|
35
|
+
const dispatch = useDispatch();
|
|
36
|
+
const selected = useMemo(() => (activeChatId ? [activeChatId] : []), [activeChatId]);
|
|
37
|
+
|
|
38
|
+
const onSelect = useCallback(
|
|
39
|
+
(info: Parameters<NonNullable<MenuProps['onSelect']>>[0]) =>
|
|
40
|
+
dispatch({ type: 'chat_open', data: { chatId: info.selectedKeys[0] } }),
|
|
41
|
+
[dispatch],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const { data: chats } = useChatList();
|
|
45
|
+
|
|
46
|
+
const items = useMemo<MenuItemType[]>(() => {
|
|
47
|
+
if (!chats) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return chats.map((x) => ({ key: x.id, label: <Chat chat={x} /> }));
|
|
52
|
+
}, [chats]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!activeChatId && chats && chats.length > 0) {
|
|
56
|
+
dispatch({ type: 'chat_open', data: { chatId: chats[0].id } });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
}, [chats, dispatch]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="wxchtf-aside">
|
|
64
|
+
{chats && <Menu className="wxchtf-chats" selectedKeys={selected} onSelect={onSelect} items={items} />}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/ui/Chat.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ChatHeader } from './ChatHeader';
|
|
2
|
+
import { ChatBody } from './ChatBody';
|
|
3
|
+
import { InputForm } from './InputForm';
|
|
4
|
+
import { useChat } from '../core';
|
|
5
|
+
import { useMe } from '../ChatifyContext';
|
|
6
|
+
|
|
7
|
+
export interface ChatProps {
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Chat(props: ChatProps) {
|
|
12
|
+
const { id } = props;
|
|
13
|
+
const me = useMe();
|
|
14
|
+
const { data: chat } = useChat({ chatId: id });
|
|
15
|
+
const isMember = chat?.members.map((x) => x.id).includes(me) === true;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<div className="wxchtf-chat">
|
|
20
|
+
<ChatHeader id={id} />
|
|
21
|
+
<ChatBody id={id} />
|
|
22
|
+
</div>
|
|
23
|
+
{isMember && <InputForm />}
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Message } from './Message';
|
|
2
|
+
import { useMessages } from '../core';
|
|
3
|
+
import { LoadMore } from './LoadMore';
|
|
4
|
+
import { SendingMessage } from './SendingMessage';
|
|
5
|
+
import { SystemMessage } from './SystemMessage';
|
|
6
|
+
import { MessageSkeleton } from './MessageSkeleton';
|
|
7
|
+
|
|
8
|
+
export interface ChatBodyProps {
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ChatBody(props: ChatBodyProps) {
|
|
13
|
+
const { id } = props;
|
|
14
|
+
const { data: messages } = useMessages({ chatId: id });
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="wxchtf-chat-body">
|
|
18
|
+
{!messages && <MessageSkeleton count={5} />}
|
|
19
|
+
{messages?.map((x, index) => {
|
|
20
|
+
if (x.type === 'message' && x.sentBy.id === 'chatify::system') {
|
|
21
|
+
return <SystemMessage text={x.text} sentAt={x.sentAt} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
switch (x.type) {
|
|
25
|
+
case 'message': {
|
|
26
|
+
return <Message message={x} key={x.id} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case 'sending': {
|
|
30
|
+
return <SendingMessage key={x.requestId} text={x.text} files={x.files} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'more': {
|
|
34
|
+
return <LoadMore key={index} chatId={id} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useChat, useSelect, ChatListItem, useDispatch } from '../core';
|
|
2
|
+
import { Avatar, Button, Col, Row, Space, Tooltip } from 'antd';
|
|
3
|
+
import { SettingOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface ChatHeaderProps {
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ChatHeader(props: ChatHeaderProps) {
|
|
11
|
+
const { id } = props;
|
|
12
|
+
const { data: chat } = useChat({ chatId: id });
|
|
13
|
+
const chatListItem = useSelect(
|
|
14
|
+
(x) => x.query['chat-list'].data?.find((x: ChatListItem) => x.id === id),
|
|
15
|
+
[id],
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const dispatch = useDispatch();
|
|
19
|
+
const onSettingsClick = useCallback(
|
|
20
|
+
() => dispatch({ type: 'chat_settings_open', data: { chatId: id } }),
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
[dispatch],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="wxchtf-chat-header">
|
|
28
|
+
<Row justify="space-between" align="middle">
|
|
29
|
+
<Col className="wxchtf-chat-name">{chat?.name ?? chatListItem?.name}</Col>
|
|
30
|
+
<Col className="wxchtf-chat-members">
|
|
31
|
+
<Space align="center">
|
|
32
|
+
{chat?.members && (
|
|
33
|
+
<Avatar.Group maxCount={5}>
|
|
34
|
+
{chat.members.map((x) => (
|
|
35
|
+
<Tooltip key={x.id} title={x.name}>
|
|
36
|
+
<Avatar src={x.avatar}>{x.name}</Avatar>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
))}
|
|
39
|
+
</Avatar.Group>
|
|
40
|
+
)}
|
|
41
|
+
<Button onClick={onSettingsClick} type="link" icon={<SettingOutlined />} />
|
|
42
|
+
</Space>
|
|
43
|
+
</Col>
|
|
44
|
+
</Row>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|