use localState for caching chats & their latest msgs

This commit is contained in:
Martti Malmi 2023-08-03 17:45:52 +03:00
parent 0e5e9e2188
commit 9e787af125
6 changed files with 68 additions and 74 deletions

View File

@ -18,7 +18,7 @@ import FeedTypeSelector from './FeedTypeSelector';
import ImageGrid from './ImageGrid';
import ShowMore from './ShowMore';
import ShowNewEvents from './ShowNewEvents';
import SortedEventMap from './SortedEventMap';
import SortedEventMap from '../../utils/SortedEventMap';
import { FeedProps, FeedState } from './types';
const INITIAL_PAGE_SIZE = 10;

View File

@ -377,6 +377,24 @@ const Events = {
}
this.zapsByNote.get(zappedNote)?.add(event);
},
async saveDM(event: Event) {
const myPub = Key.getPubKey();
let user = event.pubkey;
if (event.pubkey === myPub) {
user = event.tags?.find((tag) => tag[0] === 'p')?.[1] || user;
} else {
const forMe = event.tags?.some((tag) => tag[0] === 'p' && tag[1] === myPub);
if (!forMe) {
return;
}
}
const latest = localState.get('chats').get(user).get('latest');
latest.once((e) => {
if (!e || !e.created_at || e.created_at < event.created_at) {
latest.put({ id: event.id, created_at: event.created_at, text: '' });
}
});
},
async handleDirectMessage(event: Event) {
const myPub = Key.getPubKey();
let user = event.pubkey;
@ -426,6 +444,7 @@ const Events = {
const byUser = this.directMessagesByUser.get(user) ?? new SortedLimitedEventSet(500);
byUser.add(event);
this.directMessagesByUser.set(user, byUser);
this.saveDM(event);
},
handleKeyValue(event: Event) {
if (event.pubkey !== Key.getPubKey()) {

View File

@ -1,6 +1,6 @@
import { Event } from 'nostr-tools';
import Events from '../../nostr/Events';
import Events from '../nostr/Events';
export default class SortedEventMap {
private eventMap: Map<string, Event>; // or should we store just strings, getting Events from loki?
@ -8,7 +8,7 @@ export default class SortedEventMap {
private sortBy: string;
private sortDirection: string;
constructor(sortBy: string, sortDirection: string) {
constructor(sortBy: string, sortDirection = 'desc') {
this.sortBy = sortBy;
this.sortDirection = sortDirection;
this.eventMap = new Map<string, Event>();

View File

@ -70,6 +70,12 @@ export default class SortedMap<K, V> {
}
}
*entries(): IterableIterator<[K, V]> {
for (const key of this.sortedKeys) {
yield [key, this.map.get(key) as V];
}
}
has(key: K): boolean {
return this.map.has(key);
}

View File

@ -2,21 +2,25 @@ import { useEffect, useRef, useState } from 'react';
import $ from 'jquery';
import localState from '../../LocalState';
import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork';
import { translate as t } from '../../translations/Translation.mjs';
import SortedMap from '../../utils/SortedMap';
import ChatListItem from './ChatListItem';
import NewChatButton from './NewChatButton';
const loadedTime = Date.now();
const sortChats = (a: { key: string; value: any }, b: { key: string; value: any }) => {
const aLatest = a.value.latest;
const bLatest = b.value.latest;
if (!aLatest) return 1;
if (!bLatest) return -1;
return bLatest.created_at - aLatest.created_at;
};
const ChatList = ({ activeChat, className }) => {
const [directMessages, setDirectMessages] = useState(new Map());
const [groups, setGroups] = useState(new Map());
const [sortedChats, setSortedChats] = useState([] as string[]);
const [shouldWait] = useState(Date.now() - loadedTime < 1000);
const [chats, setChats] = useState(new SortedMap<string, any>(sortChats) as any);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_renderCount, setRenderCount] = useState(0); // new state variable
const chatListRef = useRef(null as any);
const enableDesktopNotifications = () => {
@ -32,31 +36,15 @@ const ChatList = ({ activeChat, className }) => {
useEffect(() => {
const unsubs = [] as any[];
const go = () => {
unsubs.push(
Events.getDirectMessages(async (incomingChats) => {
let keys = Array.from(incomingChats.keys()) as string[];
const maxFollowDistance = await localState
.get('globalFilter')
.get('maxFollowDistance')
.once();
const blockedUsers = Object.keys((await localState.get('blockedUsers').once()) || {});
keys = keys.filter(
(key: string) =>
!blockedUsers.includes(key) &&
SocialNetwork.getFollowDistance(key) <= maxFollowDistance,
);
const filteredChats = new Map(keys.map((k) => [k, incomingChats.get(k)]));
setDirectMessages(filteredChats);
}),
);
};
if (shouldWait) {
setTimeout(go, 1000); // timeout always helps
} else {
go();
}
unsubs.push(
localState.get('chats').map((value, key) => {
setChats((prevChats) => {
prevChats.set(key, { ...value });
return prevChats;
});
setRenderCount((prevCount) => prevCount + 1);
}),
);
unsubs.push(
localState.get('scrollUp').on(() => {
@ -67,6 +55,7 @@ const ChatList = ({ activeChat, className }) => {
}),
);
/*
unsubs.push(
localState.get('groups').map((group, localKey) => {
if (!(group && localKey)) return;
@ -74,27 +63,11 @@ const ChatList = ({ activeChat, className }) => {
setGroups((prevGroups) => new Map(prevGroups.set(localKey, group)));
}),
);
*/
return () => unsubs.forEach((unsub) => unsub());
}, []);
useEffect(() => {
const chats: Map<string, any> = new Map(directMessages);
groups.forEach((value, key) => {
chats.set(key, value);
});
const sorted = Array.from(chats.keys()).sort((a, b) => {
if (a.length < b.length) return -1; // show groups first until their last msg is implemented
const aEventIds = chats.get(a).eventIds;
const bEventIds = chats.get(b).eventIds;
const aLatestEvent = aEventIds.length ? Events.db.by('id', aEventIds[0]) : null;
const bLatestEvent = bEventIds.length ? Events.db.by('id', bEventIds[0]) : null;
return bLatestEvent?.created_at - aLatestEvent?.created_at;
}) as string[];
setSortedChats(sorted);
}, [directMessages, groups]);
const activeChatHex = (activeChat && Key.toNostrHexAddress(activeChat)) || activeChat;
return (
@ -110,12 +83,12 @@ const ChatList = ({ activeChat, className }) => {
</div>
<div className="flex flex-1 flex-col">
<NewChatButton active={activeChatHex === 'new'} />
{sortedChats.map((pubkey) => (
{Array.from<[string, any]>(chats.entries() as any).map(([pubkey, data]) => (
<ChatListItem
active={pubkey === activeChatHex}
key={pubkey}
chat={pubkey}
latestMsgId={directMessages.get(pubkey)?.eventIds[0]}
latestMsg={data?.latest}
/>
))}
</div>

View File

@ -11,10 +11,9 @@ import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import { translate as t } from '../../translations/Translation.mjs';
const ChatListItem = ({ chat, active = false, latestMsgId = null }) => {
const ChatListItem = ({ chat, active = false, latestMsg = {} as any }) => {
const [name, setName] = useState(null);
const [latest, setLatest] = useState({} as any);
const [latestText, setLatestText] = useState('');
const [latestText, setLatestText] = useState(latestMsg.text || '');
useEffect(() => {
const isGroup = chat.length < 20;
@ -28,12 +27,21 @@ const ChatListItem = ({ chat, active = false, latestMsgId = null }) => {
}
});
}
getLatestMsg();
}, [chat]);
useEffect(() => {
getLatestMsg();
}, [latestMsgId]);
if (latestMsg.text || !latestMsg.id) {
return;
}
Events.getEventById(latestMsg.id, false, (event) => {
if (event) {
Key.decryptMessage(latestMsg.id, (text: string) => {
setLatestText(text);
localState.get('chats').get(chat).get('latest').get('text').put(text);
});
}
});
}, [latestMsg.id]);
const onKeyUp = (e: KeyboardEvent) => {
// if enter was pressed, click the element
@ -42,22 +50,10 @@ const ChatListItem = ({ chat, active = false, latestMsgId = null }) => {
}
};
const getLatestMsg = () => {
if (!latestMsgId) {
return;
}
const event = Events.db.by('id', latestMsgId);
if (event) {
setLatest(event);
Key.decryptMessage(latestMsgId, (text: string) => {
setLatestText(text);
});
}
};
const activeClass = active ? 'bg-neutral-800' : 'hover:bg-neutral-900';
const time =
(latest.created_at && Helpers.getRelativeTimeText(new Date(latest.created_at * 1000))) || '';
(latestMsg.created_at && Helpers.getRelativeTimeText(new Date(latestMsg.created_at * 1000))) ||
'';
const npub = Key.toNostrBech32Address(chat, 'npub');