@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,59 @@
|
|
|
1
|
+
import { Button, Col, List, Row, Select } from 'antd';
|
|
2
|
+
import { Account, useAccounts, useAddMember, useChat, useSelect } from '../core';
|
|
3
|
+
import { CloseOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useRemoveMember } from '../core';
|
|
5
|
+
import { useCallback, useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
function Member(props: { account: Account; chatId: string }) {
|
|
8
|
+
const { account, chatId } = props;
|
|
9
|
+
const [remove] = useRemoveMember();
|
|
10
|
+
|
|
11
|
+
const onRemoveClick = useCallback(
|
|
12
|
+
() => remove({ chatId, accountId: account.id, deleteHistory: true }),
|
|
13
|
+
[remove, chatId, account.id],
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<List.Item>
|
|
18
|
+
{account.name} <Button onClick={onRemoveClick} type="link" icon={<CloseOutlined />} />
|
|
19
|
+
</List.Item>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ChatSettings() {
|
|
24
|
+
const [chatId] = useSelect((x) => [x.ui.chatId], []);
|
|
25
|
+
const { data: chat } = useChat({ chatId: chatId! });
|
|
26
|
+
const [add] = useAddMember();
|
|
27
|
+
const { data: accounts } = useAccounts();
|
|
28
|
+
const accountOptions = useMemo(() => accounts?.map((x) => ({ value: x.id, label: x.name })), [accounts]);
|
|
29
|
+
|
|
30
|
+
const onAdd = useCallback(
|
|
31
|
+
(id: string) => add({ chatId: chatId!, accountId: id, withHistory: true }),
|
|
32
|
+
[add, chatId],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!chat) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="wxchtf-chat-settings">
|
|
41
|
+
<Row justify="space-between">
|
|
42
|
+
<Col>
|
|
43
|
+
<h3 className="wxchtf-title">{chat.name}</h3>
|
|
44
|
+
</Col>
|
|
45
|
+
<Col>
|
|
46
|
+
<Button type="link" icon={<CloseOutlined />} />
|
|
47
|
+
</Col>
|
|
48
|
+
</Row>
|
|
49
|
+
<h4>Members</h4>
|
|
50
|
+
<List
|
|
51
|
+
dataSource={chat.members}
|
|
52
|
+
rowKey="id"
|
|
53
|
+
renderItem={(account) => <Member account={account} chatId={chatId!} />}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
<Select<string> style={{ width: '350px' }} options={accountOptions} value={null} onSelect={onAdd} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CSSProperties, useCallback } from 'react';
|
|
2
|
+
import { useSignalR, ReducerProvider, useDispatch, useReadMonitor } from '../core';
|
|
3
|
+
import { Aside } from './Aside';
|
|
4
|
+
import { Main } from './Main';
|
|
5
|
+
import { Button } from 'antd';
|
|
6
|
+
import { Localizer, LocalizerContext, defaultLocalizer } from './localizer';
|
|
7
|
+
|
|
8
|
+
export interface ChatifyProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: CSSProperties;
|
|
11
|
+
localizer?: Localizer;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Content(props: Pick<ChatifyProps, 'className' | 'style'>) {
|
|
15
|
+
const { className = '', style } = props;
|
|
16
|
+
useSignalR();
|
|
17
|
+
useReadMonitor();
|
|
18
|
+
const dispatch = useDispatch();
|
|
19
|
+
const onAddChat = useCallback(() => dispatch({ type: 'new_chat_open', data: undefined }), [dispatch]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={className + ' wxchtf-chatify'} style={style}>
|
|
23
|
+
<div className="wxchtf-header">
|
|
24
|
+
<Button onClick={onAddChat} icon={'+'} type="link">
|
|
25
|
+
Add chat
|
|
26
|
+
</Button>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="wxchtf-body">
|
|
29
|
+
<Aside />
|
|
30
|
+
<Main />
|
|
31
|
+
</div>
|
|
32
|
+
<div className="wxchtf-footer"></div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Chatify(props: ChatifyProps) {
|
|
38
|
+
const { localizer = defaultLocalizer } = props;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<LocalizerContext.Provider value={localizer}>
|
|
42
|
+
<ReducerProvider>
|
|
43
|
+
<Content {...props} />
|
|
44
|
+
</ReducerProvider>
|
|
45
|
+
</LocalizerContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Button, Input } from 'antd';
|
|
2
|
+
import SendOutlined from '@ant-design/icons/SendOutlined';
|
|
3
|
+
import FileAddOutlined from '@ant-design/icons/FileAddOutlined';
|
|
4
|
+
import { useCallback, useState } from 'react';
|
|
5
|
+
import isHotkey from 'is-hotkey';
|
|
6
|
+
import { useAddMessage } from '../core/useAddMessage';
|
|
7
|
+
import { uniqId } from '../util';
|
|
8
|
+
import { useSelect } from '../core';
|
|
9
|
+
|
|
10
|
+
export function InputForm() {
|
|
11
|
+
const chatId = useSelect((x) => x.ui.chatId!, []);
|
|
12
|
+
const [add] = useAddMessage({ chatId });
|
|
13
|
+
const [text, setText] = useState('');
|
|
14
|
+
|
|
15
|
+
const onSend = useCallback(() => {
|
|
16
|
+
setText('');
|
|
17
|
+
add({ text: text, files: [], requestId: uniqId() });
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20
|
+
}, [chatId, text]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="wxchtf-input-form">
|
|
24
|
+
<div className="wxchtf-input-box">
|
|
25
|
+
<div className="wxchtf-add-files-box">
|
|
26
|
+
<Button type="link" icon={<FileAddOutlined />} className="wxchtf-add-files-btn" />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="wxchtf-text-input-box">
|
|
29
|
+
<Input.TextArea
|
|
30
|
+
value={text}
|
|
31
|
+
onChange={(e) => setText(e.target.value)}
|
|
32
|
+
autoSize={{ maxRows: 5 }}
|
|
33
|
+
className="wxchtf-text-input-textarea"
|
|
34
|
+
placeholder="Start typing (Ctrl/⌘Cmd + Enter to send) ..."
|
|
35
|
+
onKeyDown={(event) => {
|
|
36
|
+
if (isHotkey('ctrl+enter')(event)) {
|
|
37
|
+
onSend();
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="wxchtf-submit-btn-box">
|
|
43
|
+
<Button onClick={onSend} type="link" icon={<SendOutlined />} className="wxchtf-submit-btn" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useLoadMore } from '../core';
|
|
3
|
+
import { MessageSkeleton } from './MessageSkeleton';
|
|
4
|
+
|
|
5
|
+
export interface LoadMoreProps {
|
|
6
|
+
chatId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function LoadMore(props: LoadMoreProps) {
|
|
10
|
+
const { chatId } = props;
|
|
11
|
+
const [loadMore, { isFetching }] = useLoadMore({ chatId });
|
|
12
|
+
const [intersecting, setIntersecting] = useState(false);
|
|
13
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
14
|
+
|
|
15
|
+
const observer = useMemo(
|
|
16
|
+
() => new IntersectionObserver(([entry]) => setIntersecting((prev) => prev || entry.isIntersecting)),
|
|
17
|
+
[],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
observer.observe(ref.current!);
|
|
22
|
+
return () => observer.disconnect();
|
|
23
|
+
}, [observer]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!intersecting) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loadMore();
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [intersecting]);
|
|
34
|
+
|
|
35
|
+
if (isFetching) {
|
|
36
|
+
return <MessageSkeleton count={4} />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return <div ref={ref} />;
|
|
40
|
+
}
|
package/src/ui/Main.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useSelect } from '../core';
|
|
2
|
+
import { AddChat } from './AddChat';
|
|
3
|
+
import { Chat } from './Chat';
|
|
4
|
+
import { ChatSettings } from './ChatSettings';
|
|
5
|
+
|
|
6
|
+
export function Main() {
|
|
7
|
+
const { view, chatId } = useSelect((x) => ({ view: x.ui.view, chatId: x.ui.chatId }), []);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="wxchtf-main">
|
|
11
|
+
{view === 'chat' && chatId && <Chat id={chatId} key={chatId} />}
|
|
12
|
+
{view === 'new-chat' && <AddChat />}
|
|
13
|
+
{view === 'chat-settings' && <ChatSettings />}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMe } from '../ChatifyContext';
|
|
2
|
+
import { MessageMd } from './MessageMd';
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { useDispatch, useSelect, Message as MessageModel } from '../core';
|
|
5
|
+
|
|
6
|
+
export interface MessageProps {
|
|
7
|
+
message: MessageModel;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Message(props: MessageProps) {
|
|
11
|
+
const { message } = props;
|
|
12
|
+
const me = useMe();
|
|
13
|
+
const my = message.sentBy.id === me;
|
|
14
|
+
const dispatch = useDispatch();
|
|
15
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
16
|
+
const readRef = useRef(false);
|
|
17
|
+
const reading = useSelect((x) => x.read.queued.includes(message.id), []);
|
|
18
|
+
|
|
19
|
+
const observer = useMemo(
|
|
20
|
+
() =>
|
|
21
|
+
new IntersectionObserver(([entry]) => {
|
|
22
|
+
if (entry.isIntersecting && !readRef.current && !message.read) {
|
|
23
|
+
readRef.current = true;
|
|
24
|
+
dispatch({ type: 'read', data: { id: message.id, timestamp: new Date().getTime() } });
|
|
25
|
+
}
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
[],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
observer.observe(ref.current!);
|
|
34
|
+
return () => observer.disconnect();
|
|
35
|
+
}, [observer]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<MessageMd
|
|
39
|
+
ref={ref}
|
|
40
|
+
my={my}
|
|
41
|
+
text={message.text}
|
|
42
|
+
files={message.files}
|
|
43
|
+
author={message.sentBy}
|
|
44
|
+
timestamp={message.sentAt}
|
|
45
|
+
read={message.read}
|
|
46
|
+
sending={false}
|
|
47
|
+
reading={reading}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Avatar } from 'antd';
|
|
2
|
+
import { Account, File } from '../core';
|
|
3
|
+
import { useLocalizer } from './localizer';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
export interface MessageMdProps {
|
|
7
|
+
author?: Account;
|
|
8
|
+
text: string;
|
|
9
|
+
files: File[];
|
|
10
|
+
my: boolean;
|
|
11
|
+
read?: boolean;
|
|
12
|
+
sending?: boolean;
|
|
13
|
+
reading?: boolean;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const MessageMd = React.forwardRef(
|
|
18
|
+
(props: MessageMdProps, forwardRef: React.ForwardedRef<HTMLDivElement>) => {
|
|
19
|
+
const { author, text, my, read, reading, sending, timestamp } = props;
|
|
20
|
+
const localizer = useLocalizer();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={forwardRef}
|
|
25
|
+
className={'wxchtf-message' + (my ? ' --my' : '') + (sending ? ' --sending' : '')}
|
|
26
|
+
>
|
|
27
|
+
{author && (
|
|
28
|
+
<div className="wxchtf-message-author">
|
|
29
|
+
<Avatar src={author.avatar} />
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
<div className="wxchtf-message-body">
|
|
33
|
+
<div className="wxchtf-message-text">{text}</div>
|
|
34
|
+
<div className="wxchtf-message-info-box">
|
|
35
|
+
<div className="wxchtf-message-sent-at">{localizer.timestamp(timestamp)}</div>
|
|
36
|
+
{read !== undefined && !my && (
|
|
37
|
+
<div className="wxchtf-message-read-box">
|
|
38
|
+
{reading ? localizer.message.reading() : localizer.message.read()}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
{sending && <div className="wxchtf-message-sending-box">{localizer.message.sending()}</div>}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Skeleton } from 'antd';
|
|
2
|
+
|
|
3
|
+
export interface MessageSkeletonProps {
|
|
4
|
+
count?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function MessageSkeleton(props: MessageSkeletonProps) {
|
|
8
|
+
const { count = 1 } = props;
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
{Array.from(Array(count)).map((_, index) => (
|
|
12
|
+
<div className="wxchtf-message-skeleton" key={index}>
|
|
13
|
+
<Skeleton.Avatar className="wxchtf-avatar" active />
|
|
14
|
+
<Skeleton.Button active className="wxchtf-content" />
|
|
15
|
+
</div>
|
|
16
|
+
))}
|
|
17
|
+
</>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { File } from '../core';
|
|
2
|
+
import { MessageMd } from './MessageMd';
|
|
3
|
+
|
|
4
|
+
export interface SendingMessageProps {
|
|
5
|
+
text: string;
|
|
6
|
+
files: File[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SendingMessage(props: SendingMessageProps) {
|
|
10
|
+
const { text, files } = props;
|
|
11
|
+
return (
|
|
12
|
+
<MessageMd text={text} files={files} my={true} sending={true} timestamp={new Date().toUTCString()} />
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Localizer, useLocalizer } from './localizer';
|
|
2
|
+
|
|
3
|
+
export interface SystemMessageProps {
|
|
4
|
+
text: string;
|
|
5
|
+
sentAt: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatSystemMessage(localizer: Localizer, text: string): React.ReactNode {
|
|
9
|
+
if (text === 'chatify://chat-created') {
|
|
10
|
+
return localizer.system.chatCreated();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
throw new Error(`Unknown system message ${text}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SystemMessage(props: SystemMessageProps) {
|
|
17
|
+
const { text, sentAt } = props;
|
|
18
|
+
const localizer = useLocalizer();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="wxchtf-system-message">
|
|
22
|
+
<div className="wxchtf-text">{formatSystemMessage(localizer, text)}</div>
|
|
23
|
+
<div className="wxchtf-timestamp">{localizer.timestamp(sentAt)}</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import React, { useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface Localizer {
|
|
5
|
+
timestamp: (value: string) => string;
|
|
6
|
+
system: {
|
|
7
|
+
chatCreated: () => string;
|
|
8
|
+
};
|
|
9
|
+
message: {
|
|
10
|
+
read: () => React.ReactNode;
|
|
11
|
+
reading: () => React.ReactNode;
|
|
12
|
+
sending: () => React.ReactNode;
|
|
13
|
+
};
|
|
14
|
+
addChat: {
|
|
15
|
+
title: () => React.ReactNode;
|
|
16
|
+
name: {
|
|
17
|
+
label: () => React.ReactNode;
|
|
18
|
+
required: () => string;
|
|
19
|
+
max250: () => string;
|
|
20
|
+
};
|
|
21
|
+
members: {
|
|
22
|
+
label: () => React.ReactNode;
|
|
23
|
+
required: () => string;
|
|
24
|
+
};
|
|
25
|
+
submitBtn: () => React.ReactNode;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const LocalizerContext = React.createContext<Localizer>(null!);
|
|
30
|
+
|
|
31
|
+
export function useLocalizer() {
|
|
32
|
+
return useContext(LocalizerContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const defaultLocalizer: Localizer = {
|
|
36
|
+
timestamp: (value: string) => dayjs(value).format('HH:mm'),
|
|
37
|
+
message: {
|
|
38
|
+
read: () => 'Read',
|
|
39
|
+
reading: () => 'Reading...',
|
|
40
|
+
sending: () => 'Sending...',
|
|
41
|
+
},
|
|
42
|
+
addChat: {
|
|
43
|
+
title: () => 'Add chat',
|
|
44
|
+
name: {
|
|
45
|
+
label: () => 'Name',
|
|
46
|
+
required: () => `Name is required`,
|
|
47
|
+
max250: () => 'Max 250 characters allowed',
|
|
48
|
+
},
|
|
49
|
+
members: {
|
|
50
|
+
label: () => 'Members',
|
|
51
|
+
required: () => 'Members is required',
|
|
52
|
+
},
|
|
53
|
+
submitBtn: () => 'Submit',
|
|
54
|
+
},
|
|
55
|
+
system: {
|
|
56
|
+
chatCreated: () => 'Chat created',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.wxchtf-aside {
|
|
2
|
+
flex: none;
|
|
3
|
+
width: 300px;
|
|
4
|
+
border-right: $border;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-flow: column nowrap;
|
|
7
|
+
|
|
8
|
+
.wxchtf-chats.ant-menu {
|
|
9
|
+
flex: 1 1 auto;
|
|
10
|
+
height: 100px;
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
|
|
13
|
+
@include scrollbar();
|
|
14
|
+
|
|
15
|
+
.ant-menu-item {
|
|
16
|
+
padding: 0;
|
|
17
|
+
height: unset;
|
|
18
|
+
margin: 0;
|
|
19
|
+
width: 100%;
|
|
20
|
+
border-radius: 0;
|
|
21
|
+
line-height: 1.5;
|
|
22
|
+
border-top: $border;
|
|
23
|
+
|
|
24
|
+
&:first-child {
|
|
25
|
+
border-top: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&.ant-menu-item-selected .wxchtf-chat-name {
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.wxchtf-chat {
|
|
33
|
+
padding: 0.5rem 1rem;
|
|
34
|
+
position: relative;
|
|
35
|
+
|
|
36
|
+
&.--unread {
|
|
37
|
+
.wxchtf-chat-last {
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.wxchtf-chat-name {
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
display: inline-block;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.wxchtf-unread-count {
|
|
48
|
+
display: inline-block;
|
|
49
|
+
padding: 0.2rem;
|
|
50
|
+
min-width: 1rem;
|
|
51
|
+
height: 1.2rem;
|
|
52
|
+
line-height: 0.7rem;
|
|
53
|
+
margin-left: 0.5rem;
|
|
54
|
+
border-radius: 50%;
|
|
55
|
+
text-align: center;
|
|
56
|
+
font-size: 0.7rem;
|
|
57
|
+
background: $unreadCountBackground;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.wxchtf-chat-last {
|
|
61
|
+
$size: 25px;
|
|
62
|
+
|
|
63
|
+
font-style: italic;
|
|
64
|
+
opacity: 0.75;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
text-overflow: ellipsis;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
line-height: $size;
|
|
69
|
+
|
|
70
|
+
.wxchtf-chat-last-author {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
margin-right: 0.5rem;
|
|
73
|
+
|
|
74
|
+
.ant-avatar {
|
|
75
|
+
display: inline-block;
|
|
76
|
+
border: none;
|
|
77
|
+
margin-right: 5px;
|
|
78
|
+
|
|
79
|
+
@include avatarSize($size);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&::after {
|
|
83
|
+
content: ':';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.wxchtf-chat-last-message {
|
|
88
|
+
display: inline;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|