@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
@@ -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,11 @@
1
+ import { useClient } from '../ChatifyContext';
2
+ import { useQuery } from './useQuery';
3
+
4
+ export function useAccounts() {
5
+ const client = useClient();
6
+
7
+ return useQuery({
8
+ key: 'accounts',
9
+ queryFn: () => client.accounts(),
10
+ });
11
+ }
@@ -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,10 @@
1
+ import { useClient } from '../ChatifyContext';
2
+ import { useQuery } from './useQuery';
3
+
4
+ export function useChatList() {
5
+ const client = useClient();
6
+ return useQuery({
7
+ key: 'chat-list',
8
+ queryFn: () => client.chats(),
9
+ });
10
+ }
@@ -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,3 @@
1
+ export * from './ui';
2
+ export * from './core';
3
+ export * from './ChatifyContext';
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }