fix loading bugs in read status service

This commit is contained in:
hzrd149
2024-08-31 10:18:43 -05:00
parent 12f984e0f5
commit 50dbdcbe55
4 changed files with 65 additions and 49 deletions

View File

@@ -24,8 +24,8 @@ class ReadStatusService {
if (ttl) this.setTTL(key, ttl); if (ttl) this.setTTL(key, ttl);
else this.setTTL(key, dayjs().add(1, "day").unix()); else this.setTTL(key, dayjs().add(1, "day").unix());
if (subject.value === undefined && !this.queue.has(key)) { if (subject.value === undefined && !this.readQueue.has(key)) {
this.queue.add(key); this.readQueue.add(key);
this.throttleRead(); this.throttleRead();
} }
@@ -37,51 +37,60 @@ class ReadStatusService {
else this.setTTL(key, dayjs().add(1, "day").unix()); else this.setTTL(key, dayjs().add(1, "day").unix());
this.status.get(key).next(read); this.status.get(key).next(read);
this.writeQueue.add(key);
this.throttleWrite(); this.throttleWrite();
} }
queue = new Set<string>(); private readQueue = new Set<string>();
private throttleRead = _throttle(this.read.bind(this), 1000); private throttleRead = _throttle(this.read.bind(this), 100);
async read() { async read() {
if (this.queue.size === 0) return; if (this.readQueue.size === 0) return;
const trans = db.transaction("read"); const trans = db.transaction("read");
this.log(`Loading ${this.queue.size} from database`); this.log(`Loading ${this.readQueue.size} from database`);
await Promise.all( await Promise.all(
Array.from(this.queue).map(async (key) => { Array.from(this.readQueue).map(async (key) => {
this.readQueue.delete(key);
const subject = this.status.get(key); const subject = this.status.get(key);
const status = await trans.store.get(key); const status = await trans.store.get(key);
this.log(key, status);
if (status) { if (status) {
subject.next(status.read); subject.next(status.read);
if (status.ttl) this.setTTL(key, status.ttl); if (status.ttl) this.setTTL(key, status.ttl);
} else subject.next(false); } else subject.next(false);
}), }),
); );
this.queue.clear();
} }
throttleWrite = _throttle(this.write.bind(this), 1000); private writeQueue = new Set<string>();
private throttleWrite = _throttle(this.write.bind(this), 100);
async write() { async write() {
if (this.writeQueue.size === 0) return;
const trans = db.transaction("read", "readwrite"); const trans = db.transaction("read", "readwrite");
let count = 0; let count = 0;
const defaultTTL = dayjs().add(1, "day").unix(); const defaultTTL = dayjs().add(1, "day").unix();
for (const [key, subject] of this.status) { for (const key of this.writeQueue) {
const subject = this.status.get(key);
if (subject.value !== undefined) { if (subject.value !== undefined) {
trans.store.put({ key, read: subject.value, ttl: this.ttl.get(key) ?? defaultTTL }); trans.store.put({ key, read: subject.value, ttl: this.ttl.get(key) ?? defaultTTL });
count++; count++;
} }
} }
this.writeQueue.clear();
await trans.done; await trans.done;
this.log(`Wrote ${count} to database`); this.log(`Wrote ${count} to database`);
} }
async prune() { async prune() {
const expired = await db.getAllKeysFromIndex("read", "ttl", IDBKeyRange.lowerBound(dayjs().unix(), true)); const expired = await db.getAllKeysFromIndex("read", "ttl", IDBKeyRange.upperBound(dayjs().unix()));
if (expired.length === 0) return; if (expired.length === 0) return;
@@ -95,7 +104,6 @@ class ReadStatusService {
const readStatusService = new ReadStatusService(); const readStatusService = new ReadStatusService();
setInterval(readStatusService.write.bind(readStatusService), 10_000);
setInterval(readStatusService.prune.bind(readStatusService), 30_000); setInterval(readStatusService.prune.bind(readStatusService), 30_000);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {

View File

@@ -1,5 +1,7 @@
import { Box, Flex, Spacer, Text, useColorModeValue } from "@chakra-ui/react";
import { PropsWithChildren, ReactNode, forwardRef, memo, useCallback, useContext, useEffect } from "react"; import { PropsWithChildren, ReactNode, forwardRef, memo, useCallback, useContext, useEffect } from "react";
import { Box, Flex, Spacer, Text, useColorModeValue } from "@chakra-ui/react";
import dayjs from "dayjs";
import UserAvatar from "../../../components/user/user-avatar"; import UserAvatar from "../../../components/user/user-avatar";
import Timestamp from "../../../components/timestamp"; import Timestamp from "../../../components/timestamp";
import UserName from "../../../components/user/user-name"; import UserName from "../../../components/user/user-name";
@@ -7,7 +9,7 @@ import { CheckIcon } from "../../../components/icons";
import FocusedContext from "../focused-context"; import FocusedContext from "../focused-context";
import useReadStatus from "../../../hooks/use-read-status"; import useReadStatus from "../../../hooks/use-read-status";
const ONE_MONTH = 60 * 60 * 24 * 30; const ONE_MONTH = dayjs().add(1, "month").unix();
type NotificationIconEntryProps = PropsWithChildren<{ type NotificationIconEntryProps = PropsWithChildren<{
icon: ReactNode; icon: ReactNode;
@@ -54,7 +56,7 @@ const NotificationIconEntry = memo(
onFocus={onClick ? undefined : focusSelf} onFocus={onClick ? undefined : focusSelf}
onClick={onClick} onClick={onClick}
userSelect="none" userSelect="none"
bg={expanded || !read ? focusColor : undefined} bg={!read ? focusColor : undefined}
> >
<Box>{icon}</Box> <Box>{icon}</Box>
<UserAvatar pubkey={pubkey} size="sm" /> <UserAvatar pubkey={pubkey} size="sm" />

View File

@@ -103,16 +103,17 @@ const NotificationsTimeline = memo(
const navigateNextUnread = () => { const navigateNextUnread = () => {
const focusedEvent = filteredEvents.find((e) => e.id === focused); const focusedEvent = filteredEvents.find((e) => e.id === focused);
if (focusedEvent) { const idx = focusedEvent ? filteredEvents.indexOf(focusedEvent) : 0;
const idx = filteredEvents.indexOf(focusedEvent); for (let i = idx; i < filteredEvents.length; i++) {
for (let i = idx; i < filteredEvents.length; i++) { if (readStatusService.getStatus(filteredEvents[i].id).value === false) {
if (readStatusService.getStatus(filteredEvents[i].id).value === false) { setFocus(filteredEvents[i].id);
setFocus(filteredEvents[i].id); break;
break;
}
} }
} }
}; };
const navigateTop = () => setFocus(filteredEvents[0]?.id ?? "");
const navigateEnd = () => setFocus(filteredEvents[filteredEvents.length - 1]?.id ?? "");
useKeyPressEvent("ArrowUp", navigatePrev); useKeyPressEvent("ArrowUp", navigatePrev);
useKeyPressEvent("ArrowDown", navigateNext); useKeyPressEvent("ArrowDown", navigateNext);
useKeyPressEvent("ArrowLeft", navigatePrev); useKeyPressEvent("ArrowLeft", navigatePrev);
@@ -121,8 +122,10 @@ const NotificationsTimeline = memo(
useKeyPressEvent("h", navigatePrev); useKeyPressEvent("h", navigatePrev);
useKeyPressEvent("j", navigateNext); useKeyPressEvent("j", navigateNext);
useKeyPressEvent("l", navigateNextUnread); useKeyPressEvent("l", navigateNextUnread);
useKeyPressEvent("H", () => setFocus(filteredEvents[0]?.id ?? "")); useKeyPressEvent("H", navigateTop);
useKeyPressEvent("L", () => setFocus(filteredEvents[filteredEvents.length - 1]?.id ?? "")); useKeyPressEvent("Home", navigateTop);
useKeyPressEvent("L", navigateEnd);
useKeyPressEvent("End", navigateEnd);
if (filteredEvents.length === 0) if (filteredEvents.length === 0)
return ( return (

View File

@@ -12,8 +12,7 @@ import { useNotifications } from "../../providers/global/notifications-provider"
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents"; import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
import { groupByRoot } from "../../helpers/notification"; import { groupByRoot } from "../../helpers/notification";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import NotificationIconEntry from "./components/notification-icon-entry"; import { ChevronLeftIcon } from "../../components/icons";
import { ChevronLeftIcon, ReplyIcon } from "../../components/icons";
import { AvatarGroup, Box, Button, ButtonGroup, Flex, LinkBox, Text, useDisclosure } from "@chakra-ui/react"; import { AvatarGroup, Box, Button, ButtonGroup, Flex, LinkBox, Text, useDisclosure } from "@chakra-ui/react";
import UserAvatarLink from "../../components/user/user-avatar-link"; import UserAvatarLink from "../../components/user/user-avatar-link";
import useSingleEvent from "../../hooks/use-single-event"; import useSingleEvent from "../../hooks/use-single-event";
@@ -27,6 +26,7 @@ import { useNavigateInDrawer } from "../../providers/drawer-sub-view-provider";
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref"; import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
import useShareableEventAddress from "../../hooks/use-shareable-event-address"; import useShareableEventAddress from "../../hooks/use-shareable-event-address";
import localSettings from "../../services/local-settings"; import localSettings from "../../services/local-settings";
import GitBranch01 from "../../components/icons/git-branch-01";
const THREAD_KINDS = [kinds.ShortTextNote, TORRENT_COMMENT_KIND]; const THREAD_KINDS = [kinds.ShortTextNote, TORRENT_COMMENT_KIND];
@@ -67,29 +67,32 @@ function ThreadGroup({ rootId, events }: { rootId: string; events: NostrEvent[]
const ref = useEventIntersectionRef(events[events.length - 1]); const ref = useEventIntersectionRef(events[events.length - 1]);
return ( return (
<NotificationIconEntry icon={<ReplyIcon boxSize={8} />}> <Flex>
<AvatarGroup size="sm"> <GitBranch01 boxSize={8} color="green.500" mr="2" />
{pubkeys.map((pubkey) => ( <Flex direction="column" gap="2">
<UserAvatarLink key={pubkey} pubkey={pubkey} /> <AvatarGroup size="sm">
{pubkeys.map((pubkey) => (
<UserAvatarLink key={pubkey} pubkey={pubkey} />
))}
</AvatarGroup>
<Box>
<Text fontWeight="bold">
{pubkeys.length > 1 ? pubkeys.length + " people" : pubkeys.length + " person"} replied in thread:
</Text>
{rootEvent && <CompactNoteContent event={rootEvent} maxLength={100} color="GrayText" />}
</Box>
{(events.length > 3 && !showAll.isOpen ? events.slice(0, 3) : events).map((event) => (
<ReplyEntry key={event.id} event={event} />
))} ))}
</AvatarGroup> {!showAll.isOpen && events.length > 3 && (
<Box> <ButtonGroup>
<Text fontWeight="bold"> <Button variant="link" py="2" onClick={showAll.onOpen} colorScheme="primary" fontWeight="bold">
{pubkeys.length > 1 ? pubkeys.length + " people" : pubkeys.length + " person"} replied in thread: +{events.length - 3} more
</Text> </Button>
{rootEvent && <CompactNoteContent event={rootEvent} maxLength={100} color="GrayText" />} </ButtonGroup>
</Box> )}
{(events.length > 3 && !showAll.isOpen ? events.slice(0, 3) : events).map((event) => ( </Flex>
<ReplyEntry key={event.id} event={event} /> </Flex>
))}
{!showAll.isOpen && events.length > 3 && (
<ButtonGroup>
<Button variant="link" py="2" onClick={showAll.onOpen} colorScheme="primary" fontWeight="bold">
+{events.length - 3} more
</Button>
</ButtonGroup>
)}
</NotificationIconEntry>
); );
} }
@@ -118,7 +121,7 @@ function ThreadsNotificationsPage() {
<IntersectionObserverProvider callback={callback}> <IntersectionObserverProvider callback={callback}>
<VerticalPageLayout> <VerticalPageLayout>
<Flex gap="2"> <Flex gap="2">
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}> <Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={() => navigate(-1)}>
Back Back
</Button> </Button>
<PeopleListSelection /> <PeopleListSelection />