feat(module): add new feature module

Add a new module called "module" that implements the following features:
- Implement feature A
- Provide API interface B
- Optimize performance issues

BREAKING CHANGE: This feature module introduces a new configuration option, requiring updates to the existing configuration files.
This commit is contained in:
kangfenmao 2024-06-18 21:01:44 +08:00
parent 9d08e77dd1
commit 182d631dd6
11 changed files with 114 additions and 49 deletions

View File

@ -30,6 +30,7 @@
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"emittery": "^1.0.3",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",

View File

@ -4,19 +4,20 @@ import {
addThread, addThread,
removeConversationFromThread, removeConversationFromThread,
removeThread, removeThread,
setActiveThread,
updateThread updateThread
} from '@renderer/store/threads' } from '@renderer/store/threads'
import { Thread } from '@renderer/types' import { Thread } from '@renderer/types'
import { useState } from 'react'
export default function useThreads() { export default function useThreads() {
const { threads, activeThread } = useAppSelector((state) => state.threads) const { threads } = useAppSelector((state) => state.threads)
const [threadId, setThreadId] = useState(threads[0]?.id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return { return {
threads, threads,
activeThread, thread: threads.find((t) => t.id === threadId),
setActiveThread: (thread: Thread) => dispatch(setActiveThread(thread)), setThread: (thread: Thread) => setThreadId(thread.id),
addThread: (thread: Thread) => dispatch(addThread(thread)), addThread: (thread: Thread) => dispatch(addThread(thread)),
removeThread: (id: string) => dispatch(removeThread({ id })), removeThread: (id: string) => dispatch(removeThread({ id })),
updateThread: (thread: Thread) => dispatch(updateThread(thread)), updateThread: (thread: Thread) => dispatch(updateThread(thread)),

View File

@ -7,11 +7,11 @@ import Threads from './components/Threads'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
const HomePage: FC = () => { const HomePage: FC = () => {
const { threads, activeThread, setActiveThread, addThread } = useThreads() const { threads, thread, setThread, addThread } = useThreads()
useEffect(() => { useEffect(() => {
!activeThread && setActiveThread(threads[0]) !thread && threads[0] && setThread(threads[0])
}, [activeThread, threads]) }, [thread, threads])
const onCreateConversation = () => { const onCreateConversation = () => {
const _thread = { const _thread = {
@ -23,7 +23,7 @@ const HomePage: FC = () => {
conversations: [] conversations: []
} }
addThread(_thread) addThread(_thread)
setActiveThread(_thread) setThread(_thread)
} }
return ( return (
@ -34,7 +34,7 @@ const HomePage: FC = () => {
<i className="iconfont icon-a-addchat"></i> <i className="iconfont icon-a-addchat"></i>
</NewButton> </NewButton>
</NavbarLeft> </NavbarLeft>
<NavbarCenter style={{ border: 'none' }}>{activeThread?.name}</NavbarCenter> <NavbarCenter style={{ border: 'none' }}>{thread?.name}</NavbarCenter>
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}> <NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<NewButton> <NewButton>
<i className="iconfont icon-showsidebarhoriz"></i> <i className="iconfont icon-showsidebarhoriz"></i>
@ -43,7 +43,7 @@ const HomePage: FC = () => {
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<Threads /> <Threads />
<Chat /> {thread && <Chat thread={thread} />}
</ContentContainer> </ContentContainer>
</Container> </Container>
) )

View File

@ -1,23 +1,24 @@
import { Message } from '@renderer/types' import { Message, Thread } from '@renderer/types'
import { FC, useEffect, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './Inputbar' import Inputbar from './Inputbar'
import Conversations from './Conversations' import Conversations from './Conversations'
import useThreads from '@renderer/hooks/useThreads' import useThreads from '@renderer/hooks/useThreads'
import { isEmpty } from 'lodash'
import localforage from 'localforage'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
const Chat: FC = () => { interface Props {
const { activeThread, addConversation } = useThreads() thread: Thread
const [messages, setMessages] = useState<Message[]>([]) }
const onSendMessage = (message: Message) => { const Chat: FC<Props> = ({ thread }) => {
setMessages([...messages, message]) const [conversationId] = useState<string>(thread.conversations[0] || uuid())
}
return ( return (
<Container> <Container>
<Conversations messages={messages}></Conversations> <Conversations thread={thread} conversationId={conversationId} />
<Inputbar onSendMessage={onSendMessage} activeThread={activeThread} /> <Inputbar thread={thread} />
</Container> </Container>
) )
} }

View File

@ -1,16 +1,58 @@
import { Message } from '@renderer/types' import { Avatar } from '@douyinfe/semi-ui'
import { FC } from 'react' import useThreads from '@renderer/hooks/useThreads'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Conversation, Message, Thread } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import localforage from 'localforage'
import { isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
messages: Message[] thread: Thread
conversationId: string
} }
const Conversations: FC<Props> = ({ messages }) => { const Conversations: FC<Props> = ({ thread, conversationId }) => {
const [messages, setMessages] = useState<Message[]>([])
const { addConversation } = useThreads()
const onSendMessage = (message: Message) => {
setMessages([...messages, message])
if (isEmpty(thread?.conversations)) {
addConversation(thread.id, conversationId)
}
localforage.setItem<Conversation>(`conversation:${conversationId}`, {
id: conversationId,
messages: [...messages, message]
})
}
useEffect(() => {
runAsyncFunction(async () => {
const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`)
conversation && setMessages(conversation.messages)
})
}, [conversationId])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)
return () => unsubscribe()
}, [onSendMessage])
return ( return (
<Container> <Container>
{messages.map((message) => ( {messages.map((message) => (
<div key={message.id}>{message.content}</div> <ConversationItem key={message.id}>
<AvatarWrapper>
<Avatar size="small" alt="Alice Swift">
Y
</Avatar>
</AvatarWrapper>
<div>{message.content}</div>
</ConversationItem>
))} ))}
</Container> </Container>
) )
@ -20,11 +62,25 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding: 15px;
overflow-y: scroll; overflow-y: scroll;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
` `
const ConversationItem = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
cursor: pointer;
&:hover {
background-color: var(--color-background-soft);
}
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
export default Conversations export default Conversations

View File

@ -1,29 +1,30 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message, Thread } from '@renderer/types' import { Message, Thread } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
activeThread: Thread thread: Thread
onSendMessage: (message: Message) => void
} }
const Inputbar: FC<Props> = ({ activeThread, onSendMessage }) => { const Inputbar: FC<Props> = ({ thread }) => {
const [text, setText] = useState('') const [text, setText] = useState('')
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
const conversationId = activeThread.conversations[0] ? activeThread.conversations[0] : uuid() const conversationId = thread.conversations[0] ? thread.conversations[0] : uuid()
const message: Message = { const message: Message = {
id: uuid(), id: uuid(),
content: text, content: text,
threadId: activeThread.id, threadId: thread.id,
conversationId, conversationId,
createdAt: 'now' createdAt: 'now'
} }
onSendMessage(message) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('') setText('')
event.preventDefault() event.preventDefault()
} }

View File

@ -5,15 +5,15 @@ import { Dropdown } from '@douyinfe/semi-ui'
import useThreads from '@renderer/hooks/useThreads' import useThreads from '@renderer/hooks/useThreads'
const Threads: FC = () => { const Threads: FC = () => {
const { threads, activeThread, setActiveThread, removeThread } = useThreads() const { threads, thread, setThread, removeThread } = useThreads()
return ( return (
<Container> <Container>
{threads.map((thread) => ( {threads.map((thread) => (
<ThreadItem <ThreadItem
key={thread.id} key={thread.id}
onClick={() => setActiveThread(thread)} onClick={() => setThread(thread)}
className={thread.id === activeThread?.id ? 'active' : ''}> className={thread.id === thread?.id ? 'active' : ''}>
<Dropdown <Dropdown
trigger="click" trigger="click"
stopPropagation stopPropagation

View File

@ -0,0 +1,7 @@
import Emittery from 'emittery'
export const EventEmitter = new Emittery()
export const EVENT_NAMES = {
SEND_MESSAGE: 'SEND_MESSAGE'
}

View File

@ -4,11 +4,9 @@ import { Thread } from '@renderer/types'
export interface ThreadsState { export interface ThreadsState {
threads: Thread[] threads: Thread[]
activeThread?: Thread
} }
const initialState: ThreadsState = { const initialState: ThreadsState = {
activeThread: getDefaultThread(),
threads: [getDefaultThread()] threads: [getDefaultThread()]
} }
@ -21,14 +19,10 @@ const threadsSlice = createSlice({
}, },
removeThread: (state, action: PayloadAction<{ id: string }>) => { removeThread: (state, action: PayloadAction<{ id: string }>) => {
state.threads = state.threads.filter((c) => c.id !== action.payload.id) state.threads = state.threads.filter((c) => c.id !== action.payload.id)
state.activeThread = state.threads[0]
}, },
updateThread: (state, action: PayloadAction<Thread>) => { updateThread: (state, action: PayloadAction<Thread>) => {
state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c)) state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c))
}, },
setActiveThread: (state, action: PayloadAction<Thread>) => {
state.activeThread = action.payload
},
addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => { addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => {
state.threads = state.threads.map((c) => state.threads = state.threads.map((c) =>
c.id === action.payload.threadId c.id === action.payload.threadId
@ -52,13 +46,7 @@ const threadsSlice = createSlice({
} }
}) })
export const { export const { addThread, removeThread, updateThread, addConversationToThread, removeConversationFromThread } =
addThread, threadsSlice.actions
removeThread,
updateThread,
setActiveThread,
addConversationToThread,
removeConversationFromThread
} = threadsSlice.actions
export default threadsSlice.reducer export default threadsSlice.reducer

View File

@ -15,6 +15,11 @@ export type Message = {
createdAt: string createdAt: string
} }
export type Conversation = {
id: string
messages: Message[]
}
export type User = { export type User = {
id: string id: string
name: string name: string

View File

@ -2264,6 +2264,11 @@ electron@^28.2.0:
"@types/node" "^18.11.18" "@types/node" "^18.11.18"
extract-zip "^2.0.1" extract-zip "^2.0.1"
emittery@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.3.tgz#c9d2a9c689870f15251bb13b31c67715c26d69ac"
integrity sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"