feat: add redux-persist
This commit is contained in:
parent
3e055b0822
commit
f866387862
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit"
|
||||||
"source.organizeImports": "explicit"
|
|
||||||
},
|
},
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/dist/**": true
|
"**/dist/**": true
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
|
||||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||||
|
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { initStore } from './store'
|
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
@ -68,8 +67,6 @@ app.whenReady().then(() => {
|
|||||||
// IPC test
|
// IPC test
|
||||||
ipcMain.on('ping', () => console.log('pong'))
|
ipcMain.on('ping', () => console.log('pong'))
|
||||||
|
|
||||||
initStore()
|
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import '@fontsource/inter'
|
import '@fontsource/inter'
|
||||||
import store from '@renderer/store'
|
import store, { persistor } from '@renderer/store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||||
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
import Sidebar from './components/app/Sidebar'
|
import Sidebar from './components/app/Sidebar'
|
||||||
import Statusbar from './components/app/Statusbar'
|
import Statusbar from './components/app/Statusbar'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
@ -11,15 +12,17 @@ import SettingsPage from './pages/settings/SettingsPage'
|
|||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<Sidebar />
|
<BrowserRouter>
|
||||||
<Routes>
|
<Sidebar />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Routes>
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
</Routes>
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
<Statusbar />
|
</Routes>
|
||||||
</BrowserRouter>
|
<Statusbar />
|
||||||
|
</BrowserRouter>
|
||||||
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import { runAsyncFunction } from '@renderer/utils'
|
|
||||||
import localforage from 'localforage'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export type Conversation = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
avatar: string
|
|
||||||
lastMessage: string
|
|
||||||
lastMessageAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useConversations() {
|
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([])
|
|
||||||
const [activeConversation, setActiveConversation] = useState<Conversation>()
|
|
||||||
|
|
||||||
// Use localforage to initialize conversations
|
|
||||||
useEffect(() => {
|
|
||||||
runAsyncFunction(async () => {
|
|
||||||
const conversations = await localforage.getItem<Conversation[]>('conversations')
|
|
||||||
conversations && setConversations(conversations)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Update localforage
|
|
||||||
useEffect(() => {
|
|
||||||
localforage.setItem('conversations', conversations)
|
|
||||||
}, [conversations])
|
|
||||||
|
|
||||||
const addConversation = (conversation) => {
|
|
||||||
setConversations([...conversations, conversation])
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeConversation = (conversationId) => {
|
|
||||||
setConversations(conversations.filter((c) => c.id !== conversationId))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConversation = (conversation) => {
|
|
||||||
setConversations(conversations.map((c) => (c.id === conversation.id ? conversation : c)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversations,
|
|
||||||
activeConversation,
|
|
||||||
setConversations,
|
|
||||||
addConversation,
|
|
||||||
removeConversation,
|
|
||||||
updateConversation,
|
|
||||||
setActiveConversation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
src/renderer/src/hooks/useThreads.ts
Normal file
17
src/renderer/src/hooks/useThreads.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { addThread, removeThread, setActiveThread, updateThread } from '@renderer/store/threads'
|
||||||
|
import { Thread } from '@renderer/types'
|
||||||
|
|
||||||
|
export default function useThreads() {
|
||||||
|
const { threads, activeThread } = useAppSelector((state) => state.threads)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
threads,
|
||||||
|
activeThread,
|
||||||
|
setActiveThread: (thread: Thread) => dispatch(setActiveThread(thread)),
|
||||||
|
addThread: (thread: Thread) => dispatch(addThread(thread)),
|
||||||
|
removeThread: (id: string) => dispatch(removeThread({ id })),
|
||||||
|
updateThread: (thread: Thread) => dispatch(updateThread(thread))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +1,21 @@
|
|||||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
import useConversations from '@renderer/hooks/useConversactions'
|
import useThreads from '@renderer/hooks/useThreads'
|
||||||
import { FC, useEffect } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Conversations from './components/Conversations'
|
|
||||||
import Chat from './components/Chat'
|
import Chat from './components/Chat'
|
||||||
|
import Threads from './components/Threads'
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const { conversations, activeConversation, setActiveConversation, addConversation } = useConversations()
|
const { threads, activeThread, setActiveThread, addThread } = useThreads()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeConversation) {
|
if (!activeThread) {
|
||||||
setActiveConversation(conversations[0])
|
setActiveThread(threads[0])
|
||||||
}
|
}
|
||||||
}, [activeConversation, conversations])
|
}, [activeThread, threads])
|
||||||
|
|
||||||
const onCreateConversation = () => {
|
const onCreateConversation = () => {
|
||||||
const _conversation = {
|
const _thread = {
|
||||||
// ID auto increment
|
// ID auto increment
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
name: 'New conversation',
|
name: 'New conversation',
|
||||||
@ -24,8 +24,8 @@ const HomePage: FC = () => {
|
|||||||
lastMessage: 'message',
|
lastMessage: 'message',
|
||||||
lastMessageAt: 'now'
|
lastMessageAt: 'now'
|
||||||
}
|
}
|
||||||
addConversation(_conversation)
|
addThread(_thread)
|
||||||
setActiveConversation(_conversation)
|
setActiveThread(_thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -40,12 +40,8 @@ const HomePage: FC = () => {
|
|||||||
<NavbarRight />
|
<NavbarRight />
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<Conversations
|
<Threads threads={threads} activeThread={activeThread} onSelectThread={setActiveThread} />
|
||||||
conversations={conversations}
|
<Chat activeThread={activeThread} />
|
||||||
activeConversation={activeConversation}
|
|
||||||
onSelectConversation={setActiveConversation}
|
|
||||||
/>
|
|
||||||
<Chat activeConversation={activeConversation} />
|
|
||||||
<Settings />
|
<Settings />
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Conversation } from '@renderer/hooks/useConversactions'
|
import { Thread } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeConversation?: Conversation
|
activeThread?: Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<Props> = ({ activeConversation }) => {
|
const Chat: FC<Props> = ({ activeThread }) => {
|
||||||
return <Container>{activeConversation?.lastMessage}</Container>
|
return <Container>{activeThread?.lastMessage}</Container>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
@ -15,6 +15,7 @@ const Container = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-right: 1px solid #ffffff20;
|
border-right: 1px solid #ffffff20;
|
||||||
|
padding: 15px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default Chat
|
export default Chat
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
import type { Conversation } from '@renderer/hooks/useConversactions'
|
import type { Thread } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversations: Conversation[]
|
threads: Thread[]
|
||||||
activeConversation?: Conversation
|
activeThread?: Thread
|
||||||
onSelectConversation: (conversation: Conversation) => void
|
onSelectThread: (conversation: Thread) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Conversations: FC<Props> = ({ conversations, activeConversation, onSelectConversation }) => {
|
const Conversations: FC<Props> = ({ threads, activeThread, onSelectThread }) => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{conversations.map((conversation) => (
|
{threads.map((thread) => (
|
||||||
<Conversation
|
<ThreadItem
|
||||||
key={conversation.id}
|
key={thread.id}
|
||||||
onClick={() => onSelectConversation(conversation)}
|
onClick={() => onSelectThread(thread)}
|
||||||
className={conversation.id === activeConversation?.id ? 'active' : ''}>
|
className={thread.id === activeThread?.id ? 'active' : ''}>
|
||||||
<ConversationTime>{conversation.lastMessageAt}</ConversationTime>
|
<ThreadTime>{thread.lastMessageAt}</ThreadTime>
|
||||||
<ConversationName>{conversation.name}</ConversationName>
|
<ThreadName>{thread.name}</ThreadName>
|
||||||
<ConversationLastMessage>{conversation.lastMessage}</ConversationLastMessage>
|
<ThreadLastMessage>{thread.lastMessage}</ThreadLastMessage>
|
||||||
</Conversation>
|
</ThreadItem>
|
||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@ -38,7 +38,7 @@ const Container = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Conversation = styled.div`
|
const ThreadItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -54,18 +54,18 @@ const Conversation = styled.div`
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ConversationTime = styled.div`
|
const ThreadTime = styled.div`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
`
|
`
|
||||||
|
|
||||||
const ConversationName = styled.div`
|
const ThreadName = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ConversationLastMessage = styled.div`
|
const ThreadLastMessage = styled.div`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
`
|
`
|
||||||
@ -1,19 +1,35 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { combineReducers, configureStore } from '@reduxjs/toolkit'
|
||||||
|
import { useDispatch, useSelector, useStore } from 'react-redux'
|
||||||
import { combineReducers } from '@reduxjs/toolkit'
|
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
|
||||||
import { useDispatch } from 'react-redux'
|
import storage from 'redux-persist/lib/storage'
|
||||||
import threads from './threads'
|
import threads from './threads'
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
|
||||||
threads
|
|
||||||
})
|
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: rootReducer
|
reducer: persistReducer(
|
||||||
|
{
|
||||||
|
key: 'cherry-ai',
|
||||||
|
storage,
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
combineReducers({
|
||||||
|
threads
|
||||||
|
})
|
||||||
|
),
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
export type AppDispatch = typeof store.dispatch
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
|
||||||
|
export const persistor = persistStore(store)
|
||||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||||
|
export const useAppStore = useStore.withTypes<typeof store>()
|
||||||
|
// export const dispatch: AppDispatch = useDispatch()
|
||||||
|
|
||||||
export default store
|
export default store
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { Thread } from '@renderer/types'
|
import { Thread } from '@renderer/types'
|
||||||
|
|
||||||
interface State {
|
export interface ThreadsState {
|
||||||
threads: Thread[]
|
threads: Thread[]
|
||||||
|
activeThread?: Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: ThreadsState = {
|
||||||
threads: []
|
threads: [],
|
||||||
|
activeThread: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadsSlice = createSlice({
|
const threadsSlice = createSlice({
|
||||||
@ -21,10 +23,13 @@ const threadsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { addThread, removeThread, updateThread } = threadsSlice.actions
|
export const { addThread, removeThread, updateThread, setActiveThread } = threadsSlice.actions
|
||||||
|
|
||||||
export default threadsSlice
|
export default threadsSlice.reducer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user