render new message indicator

This commit is contained in:
Roland Osborne 2022-07-11 00:19:59 -07:00
parent c2783fe693
commit 9e3074132d
9 changed files with 170 additions and 53 deletions

View File

@ -7,6 +7,7 @@ import { GroupContextProvider } from 'context/GroupContext';
import { CardContextProvider } from 'context/CardContext'; import { CardContextProvider } from 'context/CardContext';
import { ChannelContextProvider } from 'context/ChannelContext'; import { ChannelContextProvider } from 'context/ChannelContext';
import { ConversationContextProvider } from 'context/ConversationContext'; import { ConversationContextProvider } from 'context/ConversationContext';
import { StoreContextProvider } from 'context/StoreContext';
import { Home } from './Home/Home'; import { Home } from './Home/Home';
import { Admin } from './Admin/Admin'; import { Admin } from './Admin/Admin';
import { Login } from './Login/Login'; import { Login } from './Login/Login';
@ -27,35 +28,37 @@ function App() {
<ArticleContextProvider> <ArticleContextProvider>
<ProfileContextProvider> <ProfileContextProvider>
<AccountContextProvider> <AccountContextProvider>
<AppContextProvider> <StoreContextProvider>
<div style={{ position: 'absolute', width: '100vw', height: '100vh', backgroundColor: '#8fbea7' }}> <AppContextProvider>
<img src={login} alt="" style={{ position: 'absolute', width: '33%', bottom: 0, right: 0 }}/> <div style={{ position: 'absolute', width: '100vw', height: '100vh', backgroundColor: '#8fbea7' }}>
</div> <img src={login} alt="" style={{ position: 'absolute', width: '33%', bottom: 0, right: 0 }}/>
<div style={{ position: 'absolute', width: '100vw', height: '100vh' }}> </div>
<Router> <div style={{ position: 'absolute', width: '100vw', height: '100vh' }}>
<Routes> <Router>
<Route path="/" element={ <Home /> } /> <Routes>
<Route path="/login" element={ <Login /> } /> <Route path="/" element={ <Home /> } />
<Route path="/admin" element={ <Admin /> } /> <Route path="/login" element={ <Login /> } />
<Route path="/create" element={ <Create /> } /> <Route path="/admin" element={ <Admin /> } />
<Route path="/user" element={ <User /> }> <Route path="/create" element={ <Create /> } />
<Route path="profile" element={<Profile />} /> <Route path="/user" element={ <User /> }>
<Route path="contact/:guid" element={<Contact />} /> <Route path="profile" element={<Profile />} />
<Route path="conversation/:cardId/:channelId" element={ <Route path="contact/:guid" element={<Contact />} />
<ConversationContextProvider> <Route path="conversation/:cardId/:channelId" element={
<Conversation /> <ConversationContextProvider>
</ConversationContextProvider> <Conversation />
} /> </ConversationContextProvider>
<Route path="conversation/:channelId" element={ } />
<ConversationContextProvider> <Route path="conversation/:channelId" element={
<Conversation /> <ConversationContextProvider>
</ConversationContextProvider> <Conversation />
} /> </ConversationContextProvider>
</Route> } />
</Routes> </Route>
</Router> </Routes>
</div> </Router>
</AppContextProvider> </div>
</AppContextProvider>
</StoreContextProvider>
</AccountContextProvider> </AccountContextProvider>
</ProfileContextProvider> </ProfileContextProvider>
</ArticleContextProvider> </ArticleContextProvider>

View File

@ -1,8 +1,7 @@
import { useContext, useState, useEffect, useRef } from 'react'; import { useContext, useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation, useParams } from "react-router-dom"; import { useNavigate, useLocation, useParams } from "react-router-dom";
import { ConversationContext } from 'context/ConversationContext'; import { ConversationContext } from 'context/ConversationContext';
import { CardContext } from 'context/CardContext'; import { StoreContext } from 'context/StoreContext';
import { ChannelContext } from 'context/ChannelContext';
export function useConversation() { export function useConversation() {
@ -18,6 +17,7 @@ export function useConversation() {
const { cardId, channelId } = useParams(); const { cardId, channelId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const conversation = useContext(ConversationContext); const conversation = useContext(ConversationContext);
const store = useContext(StoreContext);
const updateState = (value) => { const updateState = (value) => {
setState((s) => ({ ...s, ...value })); setState((s) => ({ ...s, ...value }));
@ -62,6 +62,11 @@ export function useConversation() {
members: conversation.state.members, members: conversation.state.members,
topics, topics,
}); });
if (conversation.state.init) {
const channel = conversation.state.channelId;
const card = conversation.state.cardId;
store.actions.setValue(`${channel}::${card}`, conversation.state.revision);
}
}, [conversation]); }, [conversation]);
return { state, actions }; return { state, actions };

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useChannelItem } from './useChannelItem.hook'; import { useChannelItem } from './useChannelItem.hook';
import { ChannelItemWrapper } from './ChannelItem.styled'; import { ChannelItemWrapper, Marker } from './ChannelItem.styled';
import { ChannelLogo } from './ChannelLogo/ChannelLogo'; import { ChannelLogo } from './ChannelLogo/ChannelLogo';
import { ChannelLabel } from './ChannelLabel/ChannelLabel'; import { ChannelLabel } from './ChannelLabel/ChannelLabel';
export function ChannelItem({ item }) { export function ChannelItem({ item }) {
let { state, actions } = useChannelItem(); let { state, actions } = useChannelItem(item);
const onSelect = () => { const onSelect = () => {
actions.select(item); actions.select(item);
@ -15,6 +15,9 @@ export function ChannelItem({ item }) {
return ( return (
<ChannelItemWrapper onClick={() => onSelect()}> <ChannelItemWrapper onClick={() => onSelect()}>
<ChannelLogo item={item} /> <ChannelLogo item={item} />
{state.updated && (
<Marker />
)}
<ChannelLabel item={item} /> <ChannelLabel item={item} />
</ChannelItemWrapper> </ChannelItemWrapper>
) )

View File

@ -1,5 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { List } from 'antd'; import { List } from 'antd';
import { StarTwoTone } from '@ant-design/icons';
export const ChannelItemWrapper = styled(List.Item)` export const ChannelItemWrapper = styled(List.Item)`
padding-left: 16px; padding-left: 16px;
@ -16,3 +17,8 @@ export const ChannelItemWrapper = styled(List.Item)`
} }
`; `;
export const Marker = styled(StarTwoTone)`
position: relative;
left: -16px;
top: 8px;
`

View File

@ -1,11 +1,25 @@
import { useContext, useState, useEffect } from 'react'; import { useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { StoreContext } from 'context/StoreContext';
export function useChannelItem() { export function useChannelItem(item) {
const [state, setState] = useState({ const [state, setState] = useState({
updated: false,
}); });
const store = useContext(StoreContext);
useEffect(() => {
let key = `${item.id}::${item.cardId}`;
if (store.state[key] == item.revision) {
updateState({ updated: false });
}
else {
updateState({ updated: true });
}
}, [store]);
const updateState = (value) => { const updateState = (value) => {
setState((s) => ({ ...s, ...value })); setState((s) => ({ ...s, ...value }));
} }

View File

@ -0,0 +1,14 @@
import { createContext } from 'react';
import { useStoreContext } from './useStoreContext.hook';
export const StoreContext = createContext({});
export function StoreContextProvider({ children }) {
const { state, actions } = useStoreContext();
return (
<StoreContext.Provider value={{ state, actions }}>
{children}
</StoreContext.Provider>
);
}

View File

@ -10,6 +10,7 @@ import { ArticleContext } from './ArticleContext';
import { GroupContext } from './GroupContext'; import { GroupContext } from './GroupContext';
import { CardContext } from './CardContext'; import { CardContext } from './CardContext';
import { ChannelContext } from './ChannelContext'; import { ChannelContext } from './ChannelContext';
import { StoreContext } from './StoreContext';
async function appCreate(username, password, token, updateState, setWebsocket) { async function appCreate(username, password, token, updateState, setWebsocket) {
await addAccount(username, password, token); await addAccount(username, password, token);
@ -58,6 +59,7 @@ export function useAppContext() {
}) })
} }
const storeContext = useContext(StoreContext);
const accountContext = useContext(AccountContext); const accountContext = useContext(AccountContext);
const profileContext = useContext(ProfileContext); const profileContext = useContext(ProfileContext);
const channelContext = useContext(ChannelContext); const channelContext = useContext(ChannelContext);
@ -79,6 +81,7 @@ export function useAppContext() {
const userActions = { const userActions = {
logout: () => { logout: () => {
appLogout(updateState, clearWebsocket); appLogout(updateState, clearWebsocket);
storeContext.actions.clear();
resetData(); resetData();
}, },
} }

View File

@ -15,6 +15,7 @@ export function useConversationContext() {
contacts: null, contacts: null,
members: new Set(), members: new Set(),
topics: new Map(), topics: new Map(),
revision: null,
}); });
const EVENT_OPEN = 1; const EVENT_OPEN = 1;
@ -44,6 +45,10 @@ export function useConversationContext() {
} }
const getSubject = (conversation) => { const getSubject = (conversation) => {
if (!conversation) {
return null;
}
try { try {
let subject = JSON.parse(conversation.data.channelDetail.data).subject; let subject = JSON.parse(conversation.data.channelDetail.data).subject;
if (subject) { if (subject) {
@ -56,6 +61,10 @@ export function useConversationContext() {
} }
const getContacts = (conversation) => { const getContacts = (conversation) => {
if (!conversation) {
return null;
}
let members = []; let members = [];
if (conversation.guid) { if (conversation.guid) {
members.push(card.actions.getCardByGuid(conversation.guid)?.data?.cardProfile?.handle); members.push(card.actions.getCardByGuid(conversation.guid)?.data?.cardProfile?.handle);
@ -70,6 +79,9 @@ export function useConversationContext() {
} }
const getMembers = (conversation) => { const getMembers = (conversation) => {
if (!conversation) {
return null;
}
let members = new Set(); let members = new Set();
if (conversation.guid) { if (conversation.guid) {
members.add(conversation.guid); members.add(conversation.guid);
@ -174,23 +186,26 @@ export function useConversationContext() {
if (channelView.current.init && deltaRevision != channelView.current.revision) { if (channelView.current.init && deltaRevision != channelView.current.revision) {
let delta = await getTopicDelta(channelView.current.revision, null, channelView.current.begin, null); let delta = await getTopicDelta(channelView.current.revision, null, channelView.current.begin, null);
await setTopicDelta(delta.topics, curView); await setTopicDelta(delta.topics, curView);
channelView.current.revision = delta.revision channelView.current.revision = delta.revision;
} }
} }
let chan = getChannel();
let subject = getSubject(chan); if (curView == view.current) {
let contacts = getContacts(chan); let chan = getChannel();
let members = getMembers(chan); let subject = getSubject(chan);
updateState({ let contacts = getContacts(chan);
init: true, let members = getMembers(chan);
subject, updateState({
contacts, init: true,
members, subject,
topics: topics.current, contacts,
}); members,
topics: topics.current,
revision: channelView.current.revision,
});
}
} }
catch (err) { catch (err) {
console.log(err);
if (!channelView.current.error) { if (!channelView.current.error) {
window.alert("This converstaion failed to update"); window.alert("This converstaion failed to update");
channelView.current.error = true; channelView.current.error = true;
@ -199,7 +214,6 @@ export function useConversationContext() {
} }
const updateConversation = async () => { const updateConversation = async () => {
if (!card.state.init || !channel.state.init) { if (!card.state.init || !channel.state.init) {
return; return;
} }
@ -208,11 +222,21 @@ export function useConversationContext() {
serialize.current++; serialize.current++;
while (events.current.length > 0) { while (events.current.length > 0) {
await setTopics(events.current[0]);
events.current.shift(); // collapse updates
while (events.current.length > 1) {
if(events.current[0].type == EVENT_UPDATE && events.current[1].type == EVENT_UPDATE) {
events.current.shift();
}
else {
break;
}
}
const ev = events.current.shift();
await setTopics(ev);
} }
updateState({ loading: false }); updateState({ loading: false });
serialize.current--; serialize.current--;
} }
}; };
@ -224,10 +248,11 @@ export function useConversationContext() {
const actions = { const actions = {
setConversationId: (cardId, channelId) => { setConversationId: (cardId, channelId) => {
view.current += 1; view.current += 1;
updateState({ loading: true }); updateState({ init: false, loading: true });
events.current = [{ type: EVENT_OPEN, data: { cardId, channelId }}]; events.current = [{ type: EVENT_OPEN, data: { cardId, channelId }}];
updateState({ init: false, subject: null, cardId, channelId, topics: new Map() }); updateState({ subject: null, cardId, channelId, topics: new Map() });
topics.current = new Map(); topics.current = new Map();
updateConversation(); updateConversation();

View File

@ -0,0 +1,44 @@
import { useEffect, useState, useRef, useContext } from 'react';
export function useStoreContext() {
const [state, setState] = useState({});
const resetState = () => {
setState((s) => {
localStorage.setItem('store', JSON.stringify({}));
return {}
});
};
const updateState = (value) => {
setState((s) => {
const store = { ...s, ...value };
localStorage.setItem('store', JSON.stringify(store));
return store;
});
};
useEffect(() => {
const store = localStorage.getItem('store');
if (store != null) {
updateState({ ...JSON.parse(store) });
}
}, []);
const actions = {
clear: () => {
resetState();
},
setValue: (key, value) => {
updateState({ [key]: value });
},
getValue: (key) => {
return state[key];
}
}
return { state, actions }
}