forked from Kieran/snort
feat: upgrade caches to worker
This commit is contained in:
parent
3c808688f8
commit
aa58ec4185
@ -23,6 +23,7 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"emojilib": "^3.0.10",
|
"emojilib": "^3.0.10",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"light-bolt11-decoder": "^2.1.0",
|
"light-bolt11-decoder": "^2.1.0",
|
||||||
|
@ -2,11 +2,7 @@ import { RelayMetricCache, UserProfileCache, UserRelaysCache } from "@snort/syst
|
|||||||
import { SnortSystemDb } from "@snort/system-web";
|
import { SnortSystemDb } from "@snort/system-web";
|
||||||
|
|
||||||
import { ChatCache } from "./ChatCache";
|
import { ChatCache } from "./ChatCache";
|
||||||
import { EventInteractionCache } from "./EventInteractionCache";
|
|
||||||
import { FollowListCache } from "./FollowListCache";
|
|
||||||
import { FollowsFeedCache } from "./FollowsFeed";
|
|
||||||
import { GiftWrapCache } from "./GiftWrapCache";
|
import { GiftWrapCache } from "./GiftWrapCache";
|
||||||
import { NotificationsCache } from "./Notifications";
|
|
||||||
import { Payments } from "./PaymentsCache";
|
import { Payments } from "./PaymentsCache";
|
||||||
|
|
||||||
export const SystemDb = new SnortSystemDb();
|
export const SystemDb = new SnortSystemDb();
|
||||||
@ -16,23 +12,15 @@ export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
|
|||||||
|
|
||||||
export const Chats = new ChatCache();
|
export const Chats = new ChatCache();
|
||||||
export const PaymentsCache = new Payments();
|
export const PaymentsCache = new Payments();
|
||||||
export const InteractionCache = new EventInteractionCache();
|
|
||||||
export const GiftsCache = new GiftWrapCache();
|
export const GiftsCache = new GiftWrapCache();
|
||||||
export const Notifications = new NotificationsCache();
|
|
||||||
export const FollowsFeed = new FollowsFeedCache();
|
|
||||||
export const FollowLists = new FollowListCache();
|
|
||||||
|
|
||||||
export async function preload(follows?: Array<string>) {
|
export async function preload(follows?: Array<string>) {
|
||||||
const preloads = [
|
const preloads = [
|
||||||
UserCache.preload(follows),
|
UserCache.preload(follows),
|
||||||
Chats.preload(),
|
Chats.preload(),
|
||||||
InteractionCache.preload(),
|
|
||||||
UserRelays.preload(follows),
|
|
||||||
RelayMetrics.preload(),
|
RelayMetrics.preload(),
|
||||||
GiftsCache.preload(),
|
GiftsCache.preload(),
|
||||||
Notifications.preload(),
|
UserRelays.preload(follows),
|
||||||
FollowsFeed.preload(),
|
|
||||||
FollowLists.preload(),
|
|
||||||
];
|
];
|
||||||
await Promise.all(preloads);
|
await Promise.all(preloads);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import { ZapperQueue } from "@/Components/Event/Note/NoteFooter/ZapperQueue";
|
|||||||
import { ZapsSummary } from "@/Components/Event/ZapsSummary";
|
import { ZapsSummary } from "@/Components/Event/ZapsSummary";
|
||||||
import ZapModal from "@/Components/ZapModal/ZapModal";
|
import ZapModal from "@/Components/ZapModal/ZapModal";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { getDisplayName } from "@/Utils";
|
import { getDisplayName } from "@/Utils";
|
||||||
import { Zapper, ZapTarget } from "@/Utils/Zapper";
|
import { Zapper, ZapTarget } from "@/Utils/Zapper";
|
||||||
@ -35,10 +34,9 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
|||||||
}));
|
}));
|
||||||
const walletState = useWallet();
|
const walletState = useWallet();
|
||||||
const wallet = walletState.wallet;
|
const wallet = walletState.wallet;
|
||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
|
||||||
const link = NostrLink.fromEvent(ev);
|
const link = NostrLink.fromEvent(ev);
|
||||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
const didZap = interactionCache.data.zapped || zaps.some(a => a.sender === publicKey);
|
const didZap = zaps.some(a => a.sender === publicKey);
|
||||||
const [showZapModal, setShowZapModal] = useState(false);
|
const [showZapModal, setShowZapModal] = useState(false);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [zapping, setZapping] = useState(false);
|
const [zapping, setZapping] = useState(false);
|
||||||
@ -102,7 +100,6 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
|||||||
if (CONFIG.features.zapPool) {
|
if (CONFIG.features.zapPool) {
|
||||||
ZapPoolController?.allocate(totalSent);
|
ZapPoolController?.allocate(totalSent);
|
||||||
}
|
}
|
||||||
await interactionCache.zap();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -143,7 +140,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
|||||||
value={zapTotal}
|
value={zapTotal}
|
||||||
onClick={fastZap}
|
onClick={fastZap}
|
||||||
/>
|
/>
|
||||||
<ZapsSummary zaps={zaps} onClick={onClickZappers} />
|
<ZapsSummary zaps={zaps} onClick={onClickZappers ?? (() => { })} />
|
||||||
</div>
|
</div>
|
||||||
{showZapModal && (
|
{showZapModal && (
|
||||||
<ZapModal
|
<ZapModal
|
||||||
|
@ -5,7 +5,6 @@ import { useIntl } from "react-intl";
|
|||||||
|
|
||||||
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
|
import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterIcon";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
export const LikeButton = ({
|
export const LikeButton = ({
|
||||||
@ -17,12 +16,10 @@ export const LikeButton = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
const { publicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
|
||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
|
|
||||||
const hasReacted = (emoji: string) => {
|
const hasReacted = (emoji: string) => {
|
||||||
return (
|
return (
|
||||||
interactionCache.data.reacted ||
|
|
||||||
positiveReactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
|
positiveReactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -31,7 +28,6 @@ export const LikeButton = ({
|
|||||||
if (!hasReacted(content) && publisher) {
|
if (!hasReacted(content) && publisher) {
|
||||||
const evLike = await publisher.react(ev, content);
|
const evLike = await publisher.react(ev, content);
|
||||||
system.BroadcastEvent(evLike);
|
system.BroadcastEvent(evLike);
|
||||||
interactionCache.react();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventReactions, useReactions } from "@snort/system-react";
|
import { useEventReactions } from "@snort/system-react";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
|
import { FooterZapButton } from "@/Components/Event/Note/NoteFooter/FooterZapButton";
|
||||||
@ -8,6 +8,7 @@ import { PowIcon } from "@/Components/Event/Note/NoteFooter/PowIcon";
|
|||||||
import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
||||||
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
||||||
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
||||||
|
import { useReactionsView } from "@/Feed/WorkerRelayView";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
export interface NoteFooterProps {
|
export interface NoteFooterProps {
|
||||||
@ -21,7 +22,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const ids = useMemo(() => [link], [link]);
|
const ids = useMemo(() => [link], [link]);
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
|
|
||||||
const related = useReactions("note:reactions", ids, undefined, false);
|
const related = useReactionsView(ids, false);
|
||||||
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
||||||
const { positive } = reactions;
|
const { positive } = reactions;
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import { AsyncFooterIcon } from "@/Components/Event/Note/NoteFooter/AsyncFooterI
|
|||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import messages from "@/Components/messages";
|
import messages from "@/Components/messages";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import { useInteractionCache } from "@/Hooks/useInteractionCache";
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { useNoteCreator } from "@/State/NoteCreator";
|
import { useNoteCreator } from "@/State/NoteCreator";
|
||||||
|
|
||||||
@ -18,11 +17,10 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
|||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.item.preferences,
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
}));
|
}));
|
||||||
const interactionCache = useInteractionCache(publicKey, ev.id);
|
|
||||||
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
|
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
|
||||||
|
|
||||||
const hasReposted = () => {
|
const hasReposted = () => {
|
||||||
return interactionCache.data.reposted || reposts.some(a => a.pubkey === publicKey);
|
return reposts.some(a => a.pubkey === publicKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const repost = async () => {
|
const repost = async () => {
|
||||||
@ -30,7 +28,6 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
|||||||
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
|
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
|
||||||
const evRepost = await publisher.repost(ev);
|
const evRepost = await publisher.repost(ev);
|
||||||
system.BroadcastEvent(evRepost);
|
system.BroadcastEvent(evRepost);
|
||||||
await interactionCache.repost();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export interface NoteTimeProps {
|
export interface NoteTimeProps {
|
||||||
@ -38,7 +38,7 @@ const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [time, setTime] = useState<string | ReactNode>(calcTime(from));
|
const [time] = useState<string | ReactNode>(calcTime(from));
|
||||||
|
|
||||||
const absoluteTime = useMemo(
|
const absoluteTime = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -51,15 +51,6 @@ const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback }) => {
|
|||||||
|
|
||||||
const isoDate = useMemo(() => new Date(from).toISOString(), [from]);
|
const isoDate = useMemo(() => new Date(from).toISOString(), [from]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const t = setInterval(() => {
|
|
||||||
const newTime = calcTime(from);
|
|
||||||
setTime(s => (s !== newTime ? newTime : s));
|
|
||||||
}, 60_000); // update every minute
|
|
||||||
|
|
||||||
return () => clearInterval(t);
|
|
||||||
}, [from]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time dateTime={isoDate} title={absoluteTime}>
|
<time dateTime={isoDate} title={absoluteTime}>
|
||||||
{time || fallback}
|
{time || fallback}
|
||||||
|
@ -2,16 +2,15 @@ import "./Timeline.css";
|
|||||||
|
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import { ReactNode, useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { FollowsFeed } from "@/Cache";
|
|
||||||
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
||||||
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
||||||
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
||||||
import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
|
import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
|
||||||
import useHashtagsFeed from "@/Feed/HashtagsFeed";
|
import useHashtagsFeed from "@/Feed/HashtagsFeed";
|
||||||
|
import { useFollowsTimelineView } from "@/Feed/WorkerRelayView";
|
||||||
import useHistoryState from "@/Hooks/useHistoryState";
|
import useHistoryState from "@/Hooks/useHistoryState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import useModeration from "@/Hooks/useModeration";
|
import useModeration from "@/Hooks/useModeration";
|
||||||
@ -35,11 +34,8 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
||||||
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
||||||
const [latest, setLatest] = useHistoryState(unixNow(), "TimelineFollowsLatest");
|
const [latest, setLatest] = useHistoryState(unixNow(), "TimelineFollowsLatest");
|
||||||
const feed = useSyncExternalStore(
|
const [limit, setLimit] = useState(50);
|
||||||
cb => FollowsFeed.hook(cb, "*"),
|
const feed = useFollowsTimelineView(limit);
|
||||||
() => FollowsFeed.snapshot(),
|
|
||||||
);
|
|
||||||
const system = useContext(SnortContext);
|
|
||||||
const { muted, isEventMuted } = useModeration();
|
const { muted, isEventMuted } = useModeration();
|
||||||
|
|
||||||
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
||||||
@ -125,7 +121,9 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
displayAs={displayAs}
|
displayAs={displayAs}
|
||||||
/>
|
/>
|
||||||
{sortedFeed.length > 0 && (
|
{sortedFeed.length > 0 && (
|
||||||
<ShowMoreInView onClick={async () => await FollowsFeed.loadMore(system, login, oldest ?? unixNow())} />
|
<ShowMoreInView onClick={() => {
|
||||||
|
setLimit(s => s + 20);
|
||||||
|
}} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { HexKey } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { FollowsFeed } from "@/Cache";
|
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
@ -24,7 +23,6 @@ export default function FollowButton(props: FollowButtonProps) {
|
|||||||
if (publisher) {
|
if (publisher) {
|
||||||
const ev = await publisher.contactList([pubkey, ...follows.item].map(a => ["p", a]));
|
const ev = await publisher.contactList([pubkey, ...follows.item].map(a => ["p", a]));
|
||||||
system.BroadcastEvent(ev);
|
system.BroadcastEvent(ev);
|
||||||
await FollowsFeed.backFill(system, [pubkey]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { HexKey } from "@snort/system";
|
|||||||
import { ReactNode, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { FollowsFeed } from "@/Cache";
|
|
||||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
@ -43,7 +42,6 @@ export default function FollowListBase({
|
|||||||
const ev = await publisher.contactList(newFollows.map(a => ["p", a]));
|
const ev = await publisher.contactList(newFollows.map(a => ["p", a]));
|
||||||
setFollows(id, newFollows, ev.created_at);
|
setFollows(id, newFollows, ev.created_at);
|
||||||
await system.BroadcastEvent(ev);
|
await system.BroadcastEvent(ev);
|
||||||
await FollowsFeed.backFill(system, pubkeys);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { EventKind, NostrLink, parseRelayTags, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
import {
|
||||||
|
EventKind,
|
||||||
|
NostrLink,
|
||||||
|
parseRelayTags,
|
||||||
|
RequestBuilder,
|
||||||
|
socialGraphInstance,
|
||||||
|
TaggedNostrEvent,
|
||||||
|
} from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { usePrevious } from "@uidotdev/usehooks";
|
import { usePrevious } from "@uidotdev/usehooks";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { FollowLists, FollowsFeed, GiftsCache, Notifications } from "@/Cache";
|
|
||||||
import { Nip4Chats, Nip28Chats } from "@/chat";
|
import { Nip4Chats, Nip28Chats } from "@/chat";
|
||||||
import { Nip28ChatSystem } from "@/chat/nip28";
|
import { Nip28ChatSystem } from "@/chat/nip28";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { useRefreshFeedCache } from "@/Hooks/useRefreshFeedcache";
|
|
||||||
import { bech32ToHex, debounce, getNewest, getNewestEventTagsByKey, unwrap } from "@/Utils";
|
import { bech32ToHex, debounce, getNewest, getNewestEventTagsByKey, unwrap } from "@/Utils";
|
||||||
import { SnortPubKey } from "@/Utils/Const";
|
import { SnortPubKey } from "@/Utils/Const";
|
||||||
import {
|
import {
|
||||||
@ -26,6 +31,8 @@ import {
|
|||||||
} from "@/Utils/Login";
|
} from "@/Utils/Login";
|
||||||
import { SubscriptionEvent } from "@/Utils/Subscription";
|
import { SubscriptionEvent } from "@/Utils/Subscription";
|
||||||
|
|
||||||
|
import { useFollowsContactListView } from "./WorkerRelayView";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managed loading data for the current logged in user
|
* Managed loading data for the current logged in user
|
||||||
*/
|
*/
|
||||||
@ -34,10 +41,10 @@ export default function useLoginFeed() {
|
|||||||
const { publicKey: pubKey, follows } = login;
|
const { publicKey: pubKey, follows } = login;
|
||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
|
|
||||||
useRefreshFeedCache(Notifications, true);
|
const followLists = useFollowsContactListView();
|
||||||
useRefreshFeedCache(FollowsFeed, true);
|
useEffect(() => {
|
||||||
useRefreshFeedCache(GiftsCache, true);
|
followLists.forEach(e => socialGraphInstance.handleEvent(e));
|
||||||
useRefreshFeedCache(FollowLists, false);
|
}, followLists);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
system.checkSigs = login.appData.item.preferences.checkSigs;
|
system.checkSigs = login.appData.item.preferences.checkSigs;
|
||||||
@ -106,7 +113,8 @@ export default function useLoginFeed() {
|
|||||||
const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]);
|
const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]);
|
||||||
setFollows(login.id, pTags, contactList.created_at * 1000);
|
setFollows(login.id, pTags, contactList.created_at * 1000);
|
||||||
|
|
||||||
FollowsFeed.backFillIfMissing(system, pTags);
|
// TODO: fixup
|
||||||
|
// FollowsFeed.backFillIfMissing(system, pTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
const relays = getNewest(loginFeed.filter(a => a.kind === EventKind.Relays));
|
const relays = getNewest(loginFeed.filter(a => a.kind === EventKind.Relays));
|
||||||
|
164
packages/app/src/Feed/WorkerRelayView.ts
Normal file
164
packages/app/src/Feed/WorkerRelayView.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { unixNow } from "@snort/shared";
|
||||||
|
import { EventKind, NostrEvent, NostrLink, ReqFilter, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import { Relay } from "@/system";
|
||||||
|
import { Day } from "@/Utils/Const";
|
||||||
|
|
||||||
|
export function useWorkerRelayView(id: string, filters: Array<ReqFilter>, maxWindow?: number) {
|
||||||
|
const [events, setEvents] = useState<Array<NostrEvent>>([]);
|
||||||
|
const [rb, setRb] = useState<RequestBuilder>();
|
||||||
|
useRequestBuilder(rb);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Relay.req([
|
||||||
|
"REQ",
|
||||||
|
`${id}+latest`,
|
||||||
|
...filters.map(f => ({
|
||||||
|
...f,
|
||||||
|
until: undefined,
|
||||||
|
since: undefined,
|
||||||
|
limit: 1,
|
||||||
|
})),
|
||||||
|
]).then(latest => {
|
||||||
|
const rb = new RequestBuilder(id);
|
||||||
|
filters
|
||||||
|
.map((f, i) => ({
|
||||||
|
...f,
|
||||||
|
limit: undefined,
|
||||||
|
until: undefined,
|
||||||
|
since: latest?.at(i)?.created_at ?? (maxWindow ? unixNow() - maxWindow : undefined),
|
||||||
|
}))
|
||||||
|
.forEach(f => rb.withBareFilter(f));
|
||||||
|
setRb(rb);
|
||||||
|
});
|
||||||
|
Relay.req(["REQ", id, ...filters]).then(setEvents);
|
||||||
|
}, [id, filters, maxWindow]);
|
||||||
|
|
||||||
|
return events as Array<TaggedNostrEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkerRelayViewCount(id: string, filters: Array<ReqFilter>, maxWindow?: number) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [rb, setRb] = useState<RequestBuilder>();
|
||||||
|
useRequestBuilder(rb);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Relay.req([
|
||||||
|
"REQ",
|
||||||
|
`${id}+latest`,
|
||||||
|
...filters.map(f => ({
|
||||||
|
...f,
|
||||||
|
until: undefined,
|
||||||
|
since: undefined,
|
||||||
|
limit: 1,
|
||||||
|
})),
|
||||||
|
]).then(latest => {
|
||||||
|
const rb = new RequestBuilder(id);
|
||||||
|
filters
|
||||||
|
.map((f, i) => ({
|
||||||
|
...f,
|
||||||
|
limit: undefined,
|
||||||
|
until: undefined,
|
||||||
|
since: latest?.at(i)?.created_at ?? (maxWindow ? unixNow() - maxWindow : undefined),
|
||||||
|
}))
|
||||||
|
.forEach(f => rb.withBareFilter(f));
|
||||||
|
setRb(rb);
|
||||||
|
});
|
||||||
|
Relay.count(["REQ", id, ...filters]).then(setCount);
|
||||||
|
}, [id, filters, maxWindow]);
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFollowsTimelineView(limit = 20) {
|
||||||
|
const follows = useLogin(s => s.follows.item);
|
||||||
|
const kinds = [EventKind.TextNote, EventKind.Repost, EventKind.Polls];
|
||||||
|
|
||||||
|
const filter = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
authors: follows,
|
||||||
|
kinds,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [follows, limit]);
|
||||||
|
return useWorkerRelayView("follows-timeline", filter, Day * 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotificationsView() {
|
||||||
|
const publicKey = useLogin(s => s.publicKey);
|
||||||
|
const kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
|
||||||
|
const req = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"#p": [publicKey ?? ""],
|
||||||
|
kinds,
|
||||||
|
since: unixNow() - Day * 7,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [publicKey]);
|
||||||
|
return useWorkerRelayView("notifications", req, Day * 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReactionsView(ids: Array<NostrLink>, leaveOpen = true) {
|
||||||
|
const req = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder("reactions");
|
||||||
|
rb.withOptions({ leaveOpen });
|
||||||
|
const grouped = ids.reduce(
|
||||||
|
(acc, v) => {
|
||||||
|
acc[v.type] ??= [];
|
||||||
|
acc[v.type].push(v);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Array<NostrLink>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [, v] of Object.entries(grouped)) {
|
||||||
|
rb.withFilter().kinds([EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]).replyToLink(v);
|
||||||
|
}
|
||||||
|
return rb.buildRaw();
|
||||||
|
}, [ids]);
|
||||||
|
|
||||||
|
return useWorkerRelayView("reactions", req, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReactionsViewCount(ids: Array<NostrLink>, leaveOpen = true) {
|
||||||
|
const req = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder("reactions");
|
||||||
|
rb.withOptions({ leaveOpen });
|
||||||
|
const grouped = ids.reduce(
|
||||||
|
(acc, v) => {
|
||||||
|
acc[v.type] ??= [];
|
||||||
|
acc[v.type].push(v);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Array<NostrLink>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [, v] of Object.entries(grouped)) {
|
||||||
|
rb.withFilter().kinds([EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]).replyToLink(v);
|
||||||
|
}
|
||||||
|
return rb.buildRaw();
|
||||||
|
}, [ids]);
|
||||||
|
|
||||||
|
return useWorkerRelayViewCount("reactions", req, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFollowsContactListView() {
|
||||||
|
const follows = useLogin(s => s.follows.item);
|
||||||
|
const kinds = [EventKind.ContactList, EventKind.Relays];
|
||||||
|
|
||||||
|
const filter = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
authors: follows,
|
||||||
|
kinds,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [follows]);
|
||||||
|
return useWorkerRelayView("follows-contacts-relays", filter, undefined);
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
import { unwrap } from "@snort/shared";
|
|
||||||
import { RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
|
|
||||||
import { RefreshFeedCache } from "@/Cache/RefreshFeedCache";
|
|
||||||
|
|
||||||
import useEventPublisher from "./useEventPublisher";
|
|
||||||
import useLogin from "./useLogin";
|
|
||||||
|
|
||||||
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
|
||||||
const login = useLogin();
|
|
||||||
const { publisher, system } = useEventPublisher();
|
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
|
||||||
if (login.publicKey) {
|
|
||||||
const rb = new RequestBuilder(`using-${c.name}`);
|
|
||||||
rb.withOptions({
|
|
||||||
leaveOpen,
|
|
||||||
});
|
|
||||||
c.buildSub(login, rb);
|
|
||||||
return rb;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [login]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sub) {
|
|
||||||
const q = system.Query(sub);
|
|
||||||
const handler = (evs: Array<TaggedNostrEvent>) => {
|
|
||||||
c.onEvent(evs, unwrap(login.publicKey), publisher);
|
|
||||||
};
|
|
||||||
q.on("event", handler);
|
|
||||||
q.uncancel();
|
|
||||||
return () => {
|
|
||||||
q.off("event", handler);
|
|
||||||
q.cancel();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [sub]);
|
|
||||||
}
|
|
@ -1,14 +1,11 @@
|
|||||||
import { useMemo, useSyncExternalStore } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { Notifications } from "@/Cache";
|
import { useNotificationsView } from "@/Feed/WorkerRelayView";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
export function HasNotificationsMarker() {
|
export function HasNotificationsMarker() {
|
||||||
const readNotifications = useLogin(s => s.readNotifications);
|
const readNotifications = useLogin(s => s.readNotifications);
|
||||||
const notifications = useSyncExternalStore(
|
const notifications = useNotificationsView();
|
||||||
c => Notifications.hook(c, "*"),
|
|
||||||
() => Notifications.snapshot(),
|
|
||||||
);
|
|
||||||
const latestNotification = useMemo(
|
const latestNotification = useMemo(
|
||||||
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||||
[notifications],
|
[notifications],
|
||||||
|
146
packages/app/src/Pages/Notifications/NotificationGroup.tsx
Normal file
146
packages/app/src/Pages/Notifications/NotificationGroup.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { unwrap } from "@snort/shared";
|
||||||
|
import { EventKind, NostrLink, parseZap, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
|
import { dedupe, getDisplayName } from "@/Utils";
|
||||||
|
import { formatShort } from "@/Utils/Number";
|
||||||
|
|
||||||
|
import { notificationContext } from "./notificationContext";
|
||||||
|
import { NotificationContext } from "./Notifications";
|
||||||
|
|
||||||
|
export function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void; }) {
|
||||||
|
const { ref, inView } = useInView({ triggerOnce: true });
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const kind = evs[0].kind;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const zaps = useMemo(() => {
|
||||||
|
return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a));
|
||||||
|
}, [evs]);
|
||||||
|
const pubkeys = dedupe(
|
||||||
|
evs.map(a => {
|
||||||
|
if (a.kind === EventKind.ZapReceipt) {
|
||||||
|
const zap = unwrap(zaps.find(b => b.id === a.id));
|
||||||
|
return zap.anonZap ? "anon" : zap.sender ?? a.pubkey;
|
||||||
|
}
|
||||||
|
return a.pubkey;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const firstPubkey = pubkeys[0];
|
||||||
|
const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : "");
|
||||||
|
const context = notificationContext(evs[0]);
|
||||||
|
const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0);
|
||||||
|
|
||||||
|
const iconName = () => {
|
||||||
|
switch (kind) {
|
||||||
|
case EventKind.Reaction:
|
||||||
|
return "heart-solid";
|
||||||
|
case EventKind.ZapReceipt:
|
||||||
|
return "zap-solid";
|
||||||
|
case EventKind.Repost:
|
||||||
|
return "repeat";
|
||||||
|
case EventKind.TextNote:
|
||||||
|
return "reverse-left";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionName = (n: number, name: string) => {
|
||||||
|
switch (kind) {
|
||||||
|
case EventKind.TextNote: {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
case EventKind.Reaction: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n,plural,=0{{name} liked} other{{name} & {n} others liked}}"
|
||||||
|
id="kuPHYE"
|
||||||
|
values={{
|
||||||
|
n,
|
||||||
|
name,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case EventKind.Repost: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
||||||
|
id="kJYo0u"
|
||||||
|
values={{
|
||||||
|
n,
|
||||||
|
name,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case EventKind.ZapReceipt: {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
||||||
|
id="Lw+I+J"
|
||||||
|
values={{
|
||||||
|
n,
|
||||||
|
name,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${kind}'d your post`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card notification-group" ref={ref}>
|
||||||
|
{inView && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col g12">
|
||||||
|
<div>
|
||||||
|
<Icon name={iconName()} size={24} className={iconName()} />
|
||||||
|
</div>
|
||||||
|
<div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-max g12">
|
||||||
|
<div className="flex">
|
||||||
|
{pubkeys
|
||||||
|
.filter(a => a !== "anon")
|
||||||
|
.slice(0, 12)
|
||||||
|
.map(v => (
|
||||||
|
<ProfileImage
|
||||||
|
key={v}
|
||||||
|
showUsername={kind === EventKind.TextNote}
|
||||||
|
pubkey={v}
|
||||||
|
size={40}
|
||||||
|
overrideUsername={v === "" ? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" }) : undefined} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{kind !== EventKind.TextNote && (
|
||||||
|
<div className="names">
|
||||||
|
{actionName(
|
||||||
|
pubkeys.length - 1,
|
||||||
|
firstPubkey === "anon"
|
||||||
|
? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" })
|
||||||
|
: getDisplayName(firstPubkeyProfile, firstPubkey)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{context && (
|
||||||
|
<NotificationContext
|
||||||
|
link={context}
|
||||||
|
onClick={() => {
|
||||||
|
if (onClick) {
|
||||||
|
onClick(context);
|
||||||
|
} else {
|
||||||
|
navigate(`/${context.encode(CONFIG.eventLinkPrefix)}`);
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,75 +1,35 @@
|
|||||||
import "./Notifications.css";
|
import "./Notifications.css";
|
||||||
|
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { EventExt, EventKind, NostrEvent, NostrLink, NostrPrefix, parseZap, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrEvent, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventFeed, useUserProfile } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
import { lazy, Suspense, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
import { lazy, Suspense, useEffect, useMemo } from "react";
|
||||||
import { useInView } from "react-intersection-observer";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Notifications } from "@/Cache";
|
|
||||||
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
import { ShowMoreInView } from "@/Components/Event/ShowMore";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
|
||||||
import { LiveEvent } from "@/Components/LiveStream/LiveEvent";
|
import { LiveEvent } from "@/Components/LiveStream/LiveEvent";
|
||||||
import PageSpinner from "@/Components/PageSpinner";
|
import PageSpinner from "@/Components/PageSpinner";
|
||||||
import Text from "@/Components/Text/Text";
|
import Text from "@/Components/Text/Text";
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
|
||||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||||
|
import { useNotificationsView } from "@/Feed/WorkerRelayView";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import useModeration from "@/Hooks/useModeration";
|
import useModeration from "@/Hooks/useModeration";
|
||||||
import { dedupe, getDisplayName, orderDescending } from "@/Utils";
|
import { orderDescending } from "@/Utils";
|
||||||
import { markNotificationsRead } from "@/Utils/Login";
|
import { markNotificationsRead } from "@/Utils/Login";
|
||||||
import { formatShort } from "@/Utils/Number";
|
|
||||||
const NotificationGraph = lazy(() => import("@/Pages/Notifications/NotificationChart"));
|
|
||||||
|
|
||||||
function notificationContext(ev: TaggedNostrEvent) {
|
import { notificationContext } from "./notificationContext";
|
||||||
switch (ev.kind) {
|
import { NotificationGroup } from "./NotificationGroup";
|
||||||
case EventKind.ZapReceipt: {
|
const NotificationGraph = lazy(() => import("@/Pages/Notifications/NotificationChart"));
|
||||||
const aTag = ev.tags.find(a => a[0] === "a");
|
|
||||||
if (aTag) {
|
|
||||||
return NostrLink.fromTag(aTag);
|
|
||||||
}
|
|
||||||
const eTag = ev.tags.find(a => a[0] === "e");
|
|
||||||
if (eTag) {
|
|
||||||
return NostrLink.fromTag(eTag);
|
|
||||||
}
|
|
||||||
const pTag = ev.tags.find(a => a[0] === "p");
|
|
||||||
if (pTag) {
|
|
||||||
return NostrLink.fromTag(pTag);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EventKind.Repost:
|
|
||||||
case EventKind.Reaction: {
|
|
||||||
const thread = EventExt.extractThread(ev);
|
|
||||||
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
|
|
||||||
if (tag.key === "e" || tag.key === "a") {
|
|
||||||
return NostrLink.fromThreadTag(tag);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown thread context");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case EventKind.TextNote: {
|
|
||||||
return NostrLink.fromEvent(ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NotificationsPage({ onClick }: { onClick?: (link: NostrLink) => void }) {
|
export default function NotificationsPage({ onClick }: { onClick?: (link: NostrLink) => void }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
const groupInterval = 3600 * 3;
|
const groupInterval = 3600 * 6;
|
||||||
const [showN, setShowN] = useState(10);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markNotificationsRead(login);
|
markNotificationsRead(login);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const notifications = useSyncExternalStore(
|
const notifications = useNotificationsView();
|
||||||
c => Notifications.hook(c, "*"),
|
|
||||||
() => Notifications.snapshot(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeKey = (ev: NostrEvent) => {
|
const timeKey = (ev: NostrEvent) => {
|
||||||
const onHour = ev.created_at - (ev.created_at % groupInterval);
|
const onHour = ev.created_at - (ev.created_at % groupInterval);
|
||||||
@ -84,9 +44,8 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
|||||||
|
|
||||||
const timeGrouped = useMemo(() => {
|
const timeGrouped = useMemo(() => {
|
||||||
return myNotifications.reduce((acc, v) => {
|
return myNotifications.reduce((acc, v) => {
|
||||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode(CONFIG.eventLinkPrefix)}:${
|
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode(CONFIG.eventLinkPrefix)}:${v.kind
|
||||||
v.kind
|
}`;
|
||||||
}`;
|
|
||||||
if (acc.has(key)) {
|
if (acc.has(key)) {
|
||||||
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
||||||
} else {
|
} else {
|
||||||
@ -106,152 +65,15 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
|||||||
)}
|
)}
|
||||||
{login.publicKey &&
|
{login.publicKey &&
|
||||||
[...timeGrouped.entries()]
|
[...timeGrouped.entries()]
|
||||||
.slice(0, showN)
|
|
||||||
.map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}
|
.map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}
|
||||||
|
|
||||||
<ShowMoreInView onClick={() => setShowN(s => Math.min(timeGrouped.size, s + 5))} />
|
<ShowMoreInView onClick={() => {}} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void }) {
|
export function NotificationContext({ link, onClick }: { link: NostrLink; onClick: () => void }) {
|
||||||
const { ref, inView } = useInView({ triggerOnce: true });
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const kind = evs[0].kind;
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const zaps = useMemo(() => {
|
|
||||||
return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a));
|
|
||||||
}, [evs]);
|
|
||||||
const pubkeys = dedupe(
|
|
||||||
evs.map(a => {
|
|
||||||
if (a.kind === EventKind.ZapReceipt) {
|
|
||||||
const zap = unwrap(zaps.find(b => b.id === a.id));
|
|
||||||
return zap.anonZap ? "anon" : zap.sender ?? a.pubkey;
|
|
||||||
}
|
|
||||||
return a.pubkey;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const firstPubkey = pubkeys[0];
|
|
||||||
const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : "");
|
|
||||||
const context = notificationContext(evs[0]);
|
|
||||||
const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0);
|
|
||||||
|
|
||||||
const iconName = () => {
|
|
||||||
switch (kind) {
|
|
||||||
case EventKind.Reaction:
|
|
||||||
return "heart-solid";
|
|
||||||
case EventKind.ZapReceipt:
|
|
||||||
return "zap-solid";
|
|
||||||
case EventKind.Repost:
|
|
||||||
return "repeat";
|
|
||||||
case EventKind.TextNote:
|
|
||||||
return "reverse-left";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionName = (n: number, name: string) => {
|
|
||||||
switch (kind) {
|
|
||||||
case EventKind.TextNote: {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
case EventKind.Reaction: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="{n,plural,=0{{name} liked} other{{name} & {n} others liked}}"
|
|
||||||
id="kuPHYE"
|
|
||||||
values={{
|
|
||||||
n,
|
|
||||||
name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case EventKind.Repost: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="{n,plural,=0{{name} reposted} other{{name} & {n} others reposted}}"
|
|
||||||
id="kJYo0u"
|
|
||||||
values={{
|
|
||||||
n,
|
|
||||||
name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case EventKind.ZapReceipt: {
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="{n,plural,=0{{name} zapped} other{{name} & {n} others zapped}}"
|
|
||||||
id="Lw+I+J"
|
|
||||||
values={{
|
|
||||||
n,
|
|
||||||
name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${kind}'d your post`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card notification-group" ref={ref}>
|
|
||||||
{inView && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col g12">
|
|
||||||
<div>
|
|
||||||
<Icon name={iconName()} size={24} className={iconName()} />
|
|
||||||
</div>
|
|
||||||
<div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-max g12">
|
|
||||||
<div className="flex">
|
|
||||||
{pubkeys
|
|
||||||
.filter(a => a !== "anon")
|
|
||||||
.slice(0, 12)
|
|
||||||
.map(v => (
|
|
||||||
<ProfileImage
|
|
||||||
key={v}
|
|
||||||
showUsername={kind === EventKind.TextNote}
|
|
||||||
pubkey={v}
|
|
||||||
size={40}
|
|
||||||
overrideUsername={v === "" ? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" }) : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{kind !== EventKind.TextNote && (
|
|
||||||
<div className="names">
|
|
||||||
{actionName(
|
|
||||||
pubkeys.length - 1,
|
|
||||||
firstPubkey === "anon"
|
|
||||||
? formatMessage({ defaultMessage: "Anon", id: "bfvyfs" })
|
|
||||||
: getDisplayName(firstPubkeyProfile, firstPubkey),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{context && (
|
|
||||||
<NotificationContext
|
|
||||||
link={context}
|
|
||||||
onClick={() => {
|
|
||||||
if (onClick) {
|
|
||||||
onClick(context);
|
|
||||||
} else {
|
|
||||||
navigate(`/${context.encode(CONFIG.eventLinkPrefix)}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationContext({ link, onClick }: { link: NostrLink; onClick: () => void }) {
|
|
||||||
const ev = useEventFeed(link);
|
const ev = useEventFeed(link);
|
||||||
if (link.type === NostrPrefix.PublicKey) {
|
if (link.type === NostrPrefix.PublicKey) {
|
||||||
return <ProfilePreview pubkey={link.id} actions={<></>} />;
|
return <ProfilePreview pubkey={link.id} actions={<></>} />;
|
||||||
|
35
packages/app/src/Pages/Notifications/notificationContext.tsx
Normal file
35
packages/app/src/Pages/Notifications/notificationContext.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { unwrap } from "@snort/shared";
|
||||||
|
import { EventExt, EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
|
export function notificationContext(ev: TaggedNostrEvent) {
|
||||||
|
switch (ev.kind) {
|
||||||
|
case EventKind.ZapReceipt: {
|
||||||
|
const aTag = ev.tags.find(a => a[0] === "a");
|
||||||
|
if (aTag) {
|
||||||
|
return NostrLink.fromTag(aTag);
|
||||||
|
}
|
||||||
|
const eTag = ev.tags.find(a => a[0] === "e");
|
||||||
|
if (eTag) {
|
||||||
|
return NostrLink.fromTag(eTag);
|
||||||
|
}
|
||||||
|
const pTag = ev.tags.find(a => a[0] === "p");
|
||||||
|
if (pTag) {
|
||||||
|
return NostrLink.fromTag(pTag);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EventKind.Repost:
|
||||||
|
case EventKind.Reaction: {
|
||||||
|
const thread = EventExt.extractThread(ev);
|
||||||
|
const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" });
|
||||||
|
if (tag.key === "e" || tag.key === "a") {
|
||||||
|
return NostrLink.fromThreadTag(tag);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown thread context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case EventKind.TextNote: {
|
||||||
|
return NostrLink.fromEvent(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,16 @@
|
|||||||
import { FeedCache } from "@snort/shared";
|
import { FeedCache } from "@snort/shared";
|
||||||
import { ReactNode, useSyncExternalStore } from "react";
|
import { ReactNode, useEffect, useState, useSyncExternalStore } from "react";
|
||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Chats,
|
Chats,
|
||||||
FollowLists,
|
|
||||||
FollowsFeed,
|
|
||||||
GiftsCache,
|
GiftsCache,
|
||||||
InteractionCache,
|
|
||||||
Notifications,
|
|
||||||
PaymentsCache,
|
PaymentsCache,
|
||||||
RelayMetrics,
|
RelayMetrics,
|
||||||
UserCache,
|
UserCache,
|
||||||
UserRelays,
|
|
||||||
} from "@/Cache";
|
} from "@/Cache";
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
|
import { Relay } from "@/system";
|
||||||
|
|
||||||
export function CacheSettings() {
|
export function CacheSettings() {
|
||||||
return (
|
return (
|
||||||
@ -22,16 +18,12 @@ export function CacheSettings() {
|
|||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage defaultMessage="Cache" id="DBiVK1" />
|
<FormattedMessage defaultMessage="Cache" id="DBiVK1" />
|
||||||
</h3>
|
</h3>
|
||||||
|
<RelayCacheStats />
|
||||||
<CacheDetails cache={UserCache} name={<FormattedMessage defaultMessage="Profiles" id="2zJXeA" />} />
|
<CacheDetails cache={UserCache} name={<FormattedMessage defaultMessage="Profiles" id="2zJXeA" />} />
|
||||||
<CacheDetails cache={UserRelays} name={<FormattedMessage defaultMessage="Relay Lists" id="tGXF0Q" />} />
|
|
||||||
<CacheDetails cache={Notifications} name={<FormattedMessage defaultMessage="Notifications" id="NAidKb" />} />
|
|
||||||
<CacheDetails cache={FollowsFeed} name={<FormattedMessage defaultMessage="Follows Feed" id="uKqSN+" />} />
|
|
||||||
<CacheDetails cache={Chats} name={<FormattedMessage defaultMessage="Chats" id="ABAQyo" />} />
|
<CacheDetails cache={Chats} name={<FormattedMessage defaultMessage="Chats" id="ABAQyo" />} />
|
||||||
<CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} />
|
<CacheDetails cache={RelayMetrics} name={<FormattedMessage defaultMessage="Relay Metrics" id="tjpYlr" />} />
|
||||||
<CacheDetails cache={PaymentsCache} name={<FormattedMessage defaultMessage="Payments" id="iYc3Ld" />} />
|
<CacheDetails cache={PaymentsCache} name={<FormattedMessage defaultMessage="Payments" id="iYc3Ld" />} />
|
||||||
<CacheDetails cache={InteractionCache} name={<FormattedMessage defaultMessage="Interactions" id="u+LyXc" />} />
|
|
||||||
<CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} />
|
<CacheDetails cache={GiftsCache} name={<FormattedMessage defaultMessage="Gift Wraps" id="fjAcWo" />} />
|
||||||
<CacheDetails cache={FollowLists} name={<FormattedMessage defaultMessage="Social Graph" id="CzHZoc" />} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,3 +57,39 @@ function CacheDetails<T>({ cache, name }: { cache: FeedCache<T>; name: ReactNode
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RelayCacheStats() {
|
||||||
|
const [counts, setCounts] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Relay.summary().then(setCounts);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between br p bg-superdark">
|
||||||
|
<div className="flex flex-col g4">
|
||||||
|
<FormattedMessage defaultMessage="Worker Relay" id="xSoIUU" />
|
||||||
|
{Object.entries(counts).map(([k, v]) => {
|
||||||
|
return <small key={k}>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n} kind {k} events"
|
||||||
|
id="I97cCX"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={v} />,
|
||||||
|
k: k
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AsyncButton onClick={() => {
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
UserMetadata,
|
UserMetadata,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
|
|
||||||
import { Chats, FollowsFeed, GiftsCache, Notifications } from "@/Cache";
|
import { Chats, GiftsCache } from "@/Cache";
|
||||||
import SnortApi from "@/External/SnortApi";
|
import SnortApi from "@/External/SnortApi";
|
||||||
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
||||||
import { Blasters } from "@/Utils/Const";
|
import { Blasters } from "@/Utils/Const";
|
||||||
@ -68,8 +68,6 @@ export function updatePreferences(id: string, p: UserPreferences) {
|
|||||||
export function logout(id: string) {
|
export function logout(id: string) {
|
||||||
LoginStore.removeSession(id);
|
LoginStore.removeSession(id);
|
||||||
GiftsCache.clear();
|
GiftsCache.clear();
|
||||||
Notifications.clear();
|
|
||||||
FollowsFeed.clear();
|
|
||||||
Chats.clear();
|
Chats.clear();
|
||||||
deleteRefCode();
|
deleteRefCode();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
@ -35,8 +35,8 @@ import SearchPage from "@/Pages/SearchPage";
|
|||||||
import SettingsRoutes from "@/Pages/settings/Routes";
|
import SettingsRoutes from "@/Pages/settings/Routes";
|
||||||
import { SubscribeRoutes } from "@/Pages/subscribe";
|
import { SubscribeRoutes } from "@/Pages/subscribe";
|
||||||
import ZapPoolPage from "@/Pages/ZapPool";
|
import ZapPoolPage from "@/Pages/ZapPool";
|
||||||
import { System } from "@/system";
|
import { initRelayWorker, System } from "@/system";
|
||||||
import { getCountry, storeRefCode, unwrap } from "@/Utils";
|
import { storeRefCode, unwrap } from "@/Utils";
|
||||||
import { LoginStore } from "@/Utils/Login";
|
import { LoginStore } from "@/Utils/Login";
|
||||||
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
|
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
|
||||||
import { Wallets } from "@/Wallet";
|
import { Wallets } from "@/Wallet";
|
||||||
@ -47,10 +47,10 @@ import { WalletReceivePage } from "./Pages/wallet/receive";
|
|||||||
import { WalletSendPage } from "./Pages/wallet/send";
|
import { WalletSendPage } from "./Pages/wallet/send";
|
||||||
|
|
||||||
async function initSite() {
|
async function initSite() {
|
||||||
console.debug(getCountry());
|
|
||||||
storeRefCode();
|
storeRefCode();
|
||||||
if (hasWasm) {
|
if (hasWasm) {
|
||||||
await wasmInit(WasmPath);
|
await wasmInit(WasmPath);
|
||||||
|
await initRelayWorker();
|
||||||
}
|
}
|
||||||
const login = LoginStore.takeSnapshot();
|
const login = LoginStore.takeSnapshot();
|
||||||
db.ready = await db.isAvailable();
|
db.ready = await db.isAvailable();
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
||||||
import { mapEventToProfile, NostrEvent, NostrSystem, ProfileLoaderService, socialGraphInstance } from "@snort/system";
|
import { mapEventToProfile, NostrEvent, NostrSystem, ProfileLoaderService, socialGraphInstance } from "@snort/system";
|
||||||
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
|
import WorkerRelayPath from "@snort/worker-relay/dist/worker?worker&url";
|
||||||
|
|
||||||
import { RelayMetrics, SystemDb, UserCache, UserRelays } from "@/Cache";
|
import { RelayMetrics, SystemDb, UserCache, UserRelays } from "@/Cache";
|
||||||
import { addCachedMetadataToFuzzySearch, addEventToFuzzySearch } from "@/Db/FuzzySearch";
|
import { addCachedMetadataToFuzzySearch, addEventToFuzzySearch } from "@/Db/FuzzySearch";
|
||||||
@ -59,6 +61,25 @@ export async function fetchProfile(key: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Relay = new WorkerRelayInterface(WorkerRelayPath);
|
||||||
|
let relayInitStarted = false;
|
||||||
|
export async function initRelayWorker() {
|
||||||
|
if (relayInitStarted) return;
|
||||||
|
relayInitStarted = true;
|
||||||
|
try {
|
||||||
|
if (await Relay.init()) {
|
||||||
|
if (await Relay.open()) {
|
||||||
|
await Relay.migrate();
|
||||||
|
System.on("event", async (_, ev) => {
|
||||||
|
await Relay.event(ev);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton user profile loader
|
* Singleton user profile loader
|
||||||
*/
|
*/
|
||||||
|
@ -10,14 +10,30 @@ export interface KeyedHookFilter {
|
|||||||
fn: HookFn;
|
fn: HookFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeedCacheEvents {
|
export interface CacheEvents {
|
||||||
change: (keys: Array<string>) => void;
|
change: (keys: Array<string>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CachedTable<T> = {
|
||||||
|
preload(): Promise<void>;
|
||||||
|
keysOnTable(): Array<string>;
|
||||||
|
getFromCache(key?: string): T | undefined;
|
||||||
|
get(key?: string): Promise<T | undefined>;
|
||||||
|
bulkGet(keys: Array<string>): Promise<Array<T>>;
|
||||||
|
set(obj: T): Promise<void>;
|
||||||
|
bulkSet(obj: Array<T> | Readonly<Array<T>>): Promise<void>;
|
||||||
|
update<TWithCreated extends T & { created: number; loaded: number }>(
|
||||||
|
m: TWithCreated,
|
||||||
|
): Promise<"new" | "refresh" | "updated" | "no_change">;
|
||||||
|
buffer(keys: Array<string>): Promise<Array<string>>;
|
||||||
|
key(of: T): string;
|
||||||
|
snapshot(): Array<T>;
|
||||||
|
} & EventEmitter<CacheEvents>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dexie backed generic hookable store
|
* Dexie backed generic hookable store
|
||||||
*/
|
*/
|
||||||
export abstract class FeedCache<TCached> extends EventEmitter<FeedCacheEvents> {
|
export abstract class FeedCache<TCached> extends EventEmitter<CacheEvents> implements CachedTable<TCached> {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
#snapshot: Array<TCached> = [];
|
#snapshot: Array<TCached> = [];
|
||||||
protected log: ReturnType<typeof debug>;
|
protected log: ReturnType<typeof debug>;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { FeedCache, removeUndefined } from "@snort/shared";
|
import { CachedTable, removeUndefined } from "@snort/shared";
|
||||||
import { SystemInterface, TaggedNostrEvent, RequestBuilder } from ".";
|
import { SystemInterface, TaggedNostrEvent, RequestBuilder } from ".";
|
||||||
|
|
||||||
export abstract class BackgroundLoader<T extends { loaded: number; created: number }> {
|
export abstract class BackgroundLoader<T extends { loaded: number; created: number }> {
|
||||||
#system: SystemInterface;
|
#system: SystemInterface;
|
||||||
readonly cache: FeedCache<T>;
|
readonly cache: CachedTable<T>;
|
||||||
#log = debug(this.name());
|
#log = debug(this.name());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,7 +17,7 @@ export abstract class BackgroundLoader<T extends { loaded: number; created: numb
|
|||||||
*/
|
*/
|
||||||
loaderFn?: (pubkeys: Array<string>) => Promise<Array<T>>;
|
loaderFn?: (pubkeys: Array<string>) => Promise<Array<T>>;
|
||||||
|
|
||||||
constructor(system: SystemInterface, cache: FeedCache<T>) {
|
constructor(system: SystemInterface, cache: CachedTable<T>) {
|
||||||
this.#system = system;
|
this.#system = system;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.#FetchMetadata();
|
this.#FetchMetadata();
|
||||||
|
@ -5,7 +5,7 @@ import { ProfileLoaderService } from "./profile-cache";
|
|||||||
import { RelayCache, RelayMetadataLoader } from "./outbox-model";
|
import { RelayCache, RelayMetadataLoader } from "./outbox-model";
|
||||||
import { Optimizer } from "./query-optimizer";
|
import { Optimizer } from "./query-optimizer";
|
||||||
import { base64 } from "@scure/base";
|
import { base64 } from "@scure/base";
|
||||||
import { FeedCache } from "@snort/shared";
|
import { CachedTable } from "@snort/shared";
|
||||||
import { ConnectionPool } from "./connection-pool";
|
import { ConnectionPool } from "./connection-pool";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import { QueryEvents } from "./query";
|
import { QueryEvents } from "./query";
|
||||||
@ -143,7 +143,7 @@ export interface SystemInterface {
|
|||||||
/**
|
/**
|
||||||
* Generic cache store for events
|
* Generic cache store for events
|
||||||
*/
|
*/
|
||||||
get eventsCache(): FeedCache<NostrEvent>;
|
get eventsCache(): CachedTable<NostrEvent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay loader loads relay metadata for a set of profiles
|
* Relay loader loads relay metadata for a set of profiles
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
|
|
||||||
import { FeedCache } from "@snort/shared";
|
import { CachedTable } from "@snort/shared";
|
||||||
import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr";
|
import { NostrEvent, TaggedNostrEvent } from "./nostr";
|
||||||
import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection";
|
import { RelaySettings, ConnectionStateSnapshot, OkResponse } from "./connection";
|
||||||
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
|
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
|
||||||
import { RelayMetricHandler } from "./relay-metric-handler";
|
import { RelayMetricHandler } from "./relay-metric-handler";
|
||||||
@ -17,7 +17,6 @@ import {
|
|||||||
RelayMetricCache,
|
RelayMetricCache,
|
||||||
UsersRelays,
|
UsersRelays,
|
||||||
SnortSystemDb,
|
SnortSystemDb,
|
||||||
EventExt,
|
|
||||||
QueryLike,
|
QueryLike,
|
||||||
} from ".";
|
} from ".";
|
||||||
import { EventsCache } from "./cache/events";
|
import { EventsCache } from "./cache/events";
|
||||||
@ -34,10 +33,10 @@ export interface NostrSystemEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NostrsystemProps {
|
export interface NostrsystemProps {
|
||||||
relayCache?: FeedCache<UsersRelays>;
|
relayCache?: CachedTable<UsersRelays>;
|
||||||
profileCache?: FeedCache<CachedMetadata>;
|
profileCache?: CachedTable<CachedMetadata>;
|
||||||
relayMetrics?: FeedCache<RelayMetrics>;
|
relayMetrics?: CachedTable<RelayMetrics>;
|
||||||
eventsCache?: FeedCache<NostrEvent>;
|
eventsCache?: CachedTable<NostrEvent>;
|
||||||
optimizer?: Optimizer;
|
optimizer?: Optimizer;
|
||||||
db?: SnortSystemDb;
|
db?: SnortSystemDb;
|
||||||
checkSigs?: boolean;
|
checkSigs?: boolean;
|
||||||
@ -53,17 +52,17 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
|
|||||||
/**
|
/**
|
||||||
* Storage class for user relay lists
|
* Storage class for user relay lists
|
||||||
*/
|
*/
|
||||||
readonly relayCache: FeedCache<UsersRelays>;
|
readonly relayCache: CachedTable<UsersRelays>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage class for user profiles
|
* Storage class for user profiles
|
||||||
*/
|
*/
|
||||||
readonly profileCache: FeedCache<CachedMetadata>;
|
readonly profileCache: CachedTable<CachedMetadata>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage class for relay metrics (connects/disconnects)
|
* Storage class for relay metrics (connects/disconnects)
|
||||||
*/
|
*/
|
||||||
readonly relayMetricsCache: FeedCache<RelayMetrics>;
|
readonly relayMetricsCache: CachedTable<RelayMetrics>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profile loading service
|
* Profile loading service
|
||||||
@ -81,7 +80,7 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
|
|||||||
readonly optimizer: Optimizer;
|
readonly optimizer: Optimizer;
|
||||||
|
|
||||||
readonly pool: ConnectionPool;
|
readonly pool: ConnectionPool;
|
||||||
readonly eventsCache: FeedCache<NostrEvent>;
|
readonly eventsCache: CachedTable<NostrEvent>;
|
||||||
readonly relayLoader: RelayMetadataLoader;
|
readonly relayLoader: RelayMetadataLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { FeedCache, unixNowMs } from "@snort/shared";
|
import { CachedTable, unixNowMs } from "@snort/shared";
|
||||||
import { RelayMetrics } from "./cache";
|
import { RelayMetrics } from "./cache";
|
||||||
import { TraceReport } from "./query";
|
import { TraceReport } from "./query";
|
||||||
|
|
||||||
export class RelayMetricHandler {
|
export class RelayMetricHandler {
|
||||||
readonly #cache: FeedCache<RelayMetrics>;
|
readonly #cache: CachedTable<RelayMetrics>;
|
||||||
|
|
||||||
constructor(cache: FeedCache<RelayMetrics>) {
|
constructor(cache: CachedTable<RelayMetrics>) {
|
||||||
this.#cache = cache;
|
this.#cache = cache;
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from "..";
|
} from "..";
|
||||||
import { NostrSystemEvents, NostrsystemProps } from "../nostr-system";
|
import { NostrSystemEvents, NostrsystemProps } from "../nostr-system";
|
||||||
import { WorkerCommand, WorkerMessage } from ".";
|
import { WorkerCommand, WorkerMessage } from ".";
|
||||||
import { FeedCache } from "@snort/shared";
|
import { CachedTable } from "@snort/shared";
|
||||||
import { EventsCache } from "../cache/events";
|
import { EventsCache } from "../cache/events";
|
||||||
import { RelayMetricHandler } from "../relay-metric-handler";
|
import { RelayMetricHandler } from "../relay-metric-handler";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
@ -31,12 +31,12 @@ export class SystemWorker extends EventEmitter<NostrSystemEvents> implements Sys
|
|||||||
#log = debug("SystemWorker");
|
#log = debug("SystemWorker");
|
||||||
#worker: Worker;
|
#worker: Worker;
|
||||||
#commandQueue: Map<string, (v: unknown) => void> = new Map();
|
#commandQueue: Map<string, (v: unknown) => void> = new Map();
|
||||||
readonly relayCache: FeedCache<UsersRelays>;
|
readonly relayCache: CachedTable<UsersRelays>;
|
||||||
readonly profileCache: FeedCache<CachedMetadata>;
|
readonly profileCache: CachedTable<CachedMetadata>;
|
||||||
readonly relayMetricsCache: FeedCache<RelayMetrics>;
|
readonly relayMetricsCache: CachedTable<RelayMetrics>;
|
||||||
readonly profileLoader: ProfileLoaderService;
|
readonly profileLoader: ProfileLoaderService;
|
||||||
readonly relayMetricsHandler: RelayMetricHandler;
|
readonly relayMetricsHandler: RelayMetricHandler;
|
||||||
readonly eventsCache: FeedCache<NostrEvent>;
|
readonly eventsCache: CachedTable<NostrEvent>;
|
||||||
readonly relayLoader: RelayMetadataLoader;
|
readonly relayLoader: RelayMetadataLoader;
|
||||||
|
|
||||||
get checkSigs() {
|
get checkSigs() {
|
||||||
|
@ -40,7 +40,15 @@ export class WorkerRelayInterface {
|
|||||||
return await this.#workerRpc<ReqCommand, Array<NostrEvent>>("req", req);
|
return await this.#workerRpc<ReqCommand, Array<NostrEvent>>("req", req);
|
||||||
}
|
}
|
||||||
|
|
||||||
#workerRpc<T, R>(cmd: string, args?: T, timeout = 5_000) {
|
async count(req: ReqCommand) {
|
||||||
|
return await this.#workerRpc<ReqCommand, number>("count", req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async summary() {
|
||||||
|
return await this.#workerRpc<void, Record<string, number>>("summary");
|
||||||
|
}
|
||||||
|
|
||||||
|
#workerRpc<T, R>(cmd: string, args?: T, timeout = 30_000) {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
const msg = {
|
const msg = {
|
||||||
id,
|
id,
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm";
|
import sqlite3InitModule, { Database, Sqlite3Static } from "@sqlite.org/sqlite-wasm";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { NostrEvent, ReqFilter } from "types";
|
import { NostrEvent, ReqFilter, unixNowMs } from "./types";
|
||||||
|
|
||||||
export class WorkerRelay {
|
export class WorkerRelay {
|
||||||
#sqlite?: Sqlite3Static;
|
#sqlite?: Sqlite3Static;
|
||||||
#log = (msg: string) => console.debug(msg);
|
#log = (...msg: Array<any>) => console.debug(...msg);
|
||||||
#db?: Database;
|
#db?: Database;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the SQLite driver
|
* Initialize the SQLite driver
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
|
if (this.#sqlite) return;
|
||||||
this.#sqlite = await sqlite3InitModule();
|
this.#sqlite = await sqlite3InitModule();
|
||||||
this.#log(`Got SQLite version: ${this.#sqlite.version.libVersion}`);
|
this.#log(`Got SQLite version: ${this.#sqlite.version.libVersion}`);
|
||||||
}
|
}
|
||||||
@ -20,15 +21,26 @@ export class WorkerRelay {
|
|||||||
*/
|
*/
|
||||||
async open(path: string) {
|
async open(path: string) {
|
||||||
if (!this.#sqlite) throw new Error("Must call init first");
|
if (!this.#sqlite) throw new Error("Must call init first");
|
||||||
|
if (this.#db) return;
|
||||||
|
|
||||||
if ("opfs" in this.#sqlite) {
|
if ("opfs" in this.#sqlite) {
|
||||||
this.#db = new this.#sqlite.oo1.OpfsDb(path, "cw");
|
try {
|
||||||
this.#log(`Opened ${this.#db.filename}`);
|
this.#db = new this.#sqlite.oo1.OpfsDb(path, "cw");
|
||||||
|
this.#log(`Opened ${this.#db.filename}`);
|
||||||
|
} catch (e) {
|
||||||
|
// wipe db
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("OPFS not supported!");
|
throw new Error("OPFS not supported!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.#db?.close();
|
||||||
|
this.#db = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do database migration
|
* Do database migration
|
||||||
*/
|
*/
|
||||||
@ -56,34 +68,7 @@ export class WorkerRelay {
|
|||||||
event(ev: NostrEvent) {
|
event(ev: NostrEvent) {
|
||||||
let eventInserted = false;
|
let eventInserted = false;
|
||||||
this.#db?.transaction(db => {
|
this.#db?.transaction(db => {
|
||||||
const legacyReplacable = [0, 3, 41];
|
eventInserted = this.#insertEvent(db, ev);
|
||||||
if (legacyReplacable.includes(ev.kind) || (ev.kind >= 10_000 && ev.kind < 20_000)) {
|
|
||||||
db.exec("delete from events where kind = ? and pubkey = ?", {
|
|
||||||
bind: [ev.kind, ev.pubkey],
|
|
||||||
});
|
|
||||||
this.#log(`Deleted old kind=${ev.kind}, author=${ev.pubkey} (rows=${db.changes()})`);
|
|
||||||
}
|
|
||||||
if (ev.kind >= 30_000 && ev.kind < 40_000) {
|
|
||||||
const dTag = ev.tags.find(a => a[0] === "d")![1];
|
|
||||||
db.exec(
|
|
||||||
"delete from events where id in (select id from events, tags where events.id = tags.event_id and tags.key = ? and tags.value = ?)",
|
|
||||||
{
|
|
||||||
bind: ["d", dTag],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.#log(`Deleted old versions of: d=${dTag}, kind=${ev.kind}, author=${ev.pubkey} (rows=${db.changes()})`);
|
|
||||||
}
|
|
||||||
db.exec("insert or ignore into events(id, pubkey, created, kind, json) values(?,?,?,?,?)", {
|
|
||||||
bind: [ev.id, ev.pubkey, ev.created_at, ev.kind, JSON.stringify(ev)],
|
|
||||||
});
|
|
||||||
eventInserted = (this.#db?.changes() as number) > 0;
|
|
||||||
if (eventInserted) {
|
|
||||||
for (const t of ev.tags.filter(a => a[0].length === 1)) {
|
|
||||||
db.exec("insert into tags(event_id, key, value) values(?, ?, ?)", {
|
|
||||||
bind: [ev.id, t[0], t[1]],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (eventInserted) {
|
if (eventInserted) {
|
||||||
this.#log(`Inserted: kind=${ev.kind},authors=${ev.pubkey},id=${ev.id}`);
|
this.#log(`Inserted: kind=${ev.kind},authors=${ev.pubkey},id=${ev.id}`);
|
||||||
@ -91,29 +76,125 @@ export class WorkerRelay {
|
|||||||
return eventInserted;
|
return eventInserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write multiple events
|
||||||
|
*/
|
||||||
|
eventBatch(evs: Array<NostrEvent>) {
|
||||||
|
let eventsInserted: Array<NostrEvent> = [];
|
||||||
|
this.#db?.transaction(db => {
|
||||||
|
for (const ev of evs) {
|
||||||
|
if (this.#insertEvent(db, ev)) {
|
||||||
|
eventsInserted.push(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (eventsInserted.length > 0) {
|
||||||
|
this.#log(`Inserted Batch: ${eventsInserted.length}/${evs.length}`);
|
||||||
|
}
|
||||||
|
return eventsInserted.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#insertEvent(db: Database, ev: NostrEvent) {
|
||||||
|
const legacyReplacable = [0, 3, 41];
|
||||||
|
if (legacyReplacable.includes(ev.kind) || (ev.kind >= 10_000 && ev.kind < 20_000)) {
|
||||||
|
db.exec("delete from events where kind = ? and pubkey = ? and created < ?", {
|
||||||
|
bind: [ev.kind, ev.pubkey, ev.created_at],
|
||||||
|
});
|
||||||
|
const oldDeleted = db.changes();
|
||||||
|
if (oldDeleted === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ev.kind >= 30_000 && ev.kind < 40_000) {
|
||||||
|
const dTag = ev.tags.find(a => a[0] === "d")![1];
|
||||||
|
db.exec(
|
||||||
|
"delete from events where id in (select id from events, tags where events.id = tags.event_id and tags.key = ? and tags.value = ?)",
|
||||||
|
{
|
||||||
|
bind: ["d", dTag],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const oldDeleted = db.changes();
|
||||||
|
if (oldDeleted === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.exec("insert or ignore into events(id, pubkey, created, kind, json) values(?,?,?,?,?)", {
|
||||||
|
bind: [ev.id, ev.pubkey, ev.created_at, ev.kind, JSON.stringify(ev)],
|
||||||
|
});
|
||||||
|
let eventInserted = (this.#db?.changes() as number) > 0;
|
||||||
|
if (eventInserted) {
|
||||||
|
for (const t of ev.tags.filter(a => a[0].length === 1)) {
|
||||||
|
db.exec("insert into tags(event_id, key, value) values(?, ?, ?)", {
|
||||||
|
bind: [ev.id, t[0], t[1]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return eventInserted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query relay by nostr filter
|
* Query relay by nostr filter
|
||||||
*/
|
*/
|
||||||
req(req: ReqFilter) {
|
req(req: ReqFilter) {
|
||||||
|
const start = unixNowMs();
|
||||||
|
|
||||||
|
const [sql, params] = this.#buildQuery(req);
|
||||||
|
const rows = this.#db?.exec(sql, {
|
||||||
|
bind: params,
|
||||||
|
returnValue: "resultRows",
|
||||||
|
});
|
||||||
|
const results = rows?.map(a => JSON.parse(a[0] as string) as NostrEvent) ?? [];
|
||||||
|
const time = unixNowMs() - start;
|
||||||
|
this.#log(`Query results took ${time.toLocaleString()}ms`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count results by nostr filter
|
||||||
|
*/
|
||||||
|
count(req: ReqFilter) {
|
||||||
|
const start = unixNowMs();
|
||||||
|
const [sql, params] = this.#buildQuery(req, true);
|
||||||
|
const rows = this.#db?.exec(sql, {
|
||||||
|
bind: params,
|
||||||
|
returnValue: "resultRows",
|
||||||
|
});
|
||||||
|
const results = (rows?.at(0)?.at(0) as number | undefined) ?? 0;
|
||||||
|
const time = unixNowMs() - start;
|
||||||
|
this.#log(`Query count results took ${time.toLocaleString()}ms`);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a summary about events table
|
||||||
|
*/
|
||||||
|
summary() {
|
||||||
|
const res = this.#db?.exec("select kind, count(*) from events group by kind order by 2 desc", {
|
||||||
|
returnValue: "resultRows",
|
||||||
|
});
|
||||||
|
return Object.fromEntries(res?.map(a => [String(a[0]), a[1] as number]) ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildQuery(req: ReqFilter, count = false) {
|
||||||
const conditions: Array<string> = [];
|
const conditions: Array<string> = [];
|
||||||
const params: Array<any> = [];
|
const params: Array<any> = [];
|
||||||
|
|
||||||
const repeatParams = (n: number) => {
|
const repeatParams = (n: number) => {
|
||||||
const ret = [];
|
const ret: Array<string> = [];
|
||||||
for (let x = 0; x < n; x++) {
|
for (let x = 0; x < n; x++) {
|
||||||
ret.push("?");
|
ret.push("?");
|
||||||
}
|
}
|
||||||
return ret.join(", ");
|
return ret.join(", ");
|
||||||
};
|
};
|
||||||
|
|
||||||
let sql = `select json from events`;
|
let sql = `select ${count ? "count(json)" : "json"} from events`;
|
||||||
const tags = Object.entries(req).filter(([k]) => k.startsWith("#"));
|
const tags = Object.entries(req).filter(([k]) => k.startsWith("#"));
|
||||||
for (const [key, values] of tags) {
|
for (const [key, values] of tags) {
|
||||||
const vArray = values as Array<string>;
|
const vArray = values as Array<string>;
|
||||||
sql += ` inner join tags on events.id = tags.event_id and tags.key = ? and tags.value in (${repeatParams(
|
sql += ` inner join tags on events.id = tags.event_id and tags.key = ? and tags.value in (${repeatParams(
|
||||||
vArray.length,
|
vArray.length,
|
||||||
)})`;
|
)})`;
|
||||||
params.push(key);
|
params.push(key.slice(1));
|
||||||
params.push(...vArray);
|
params.push(...vArray);
|
||||||
}
|
}
|
||||||
if (req.ids) {
|
if (req.ids) {
|
||||||
@ -142,13 +223,7 @@ export class WorkerRelay {
|
|||||||
if (req.limit) {
|
if (req.limit) {
|
||||||
sql += ` order by created desc limit ${req.limit}`;
|
sql += ` order by created desc limit ${req.limit}`;
|
||||||
}
|
}
|
||||||
|
return [sql, params];
|
||||||
this.#log(`Made query ${sql} from ${JSON.stringify(req)}`);
|
|
||||||
const rows = this.#db?.exec(sql, {
|
|
||||||
bind: params,
|
|
||||||
returnValue: "resultRows",
|
|
||||||
});
|
|
||||||
return rows?.map(a => JSON.parse(a[0] as string) as NostrEvent) ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#migrate_v1() {
|
#migrate_v1() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface WorkerMessage<T> {
|
export interface WorkerMessage<T> {
|
||||||
id: string;
|
id: string;
|
||||||
cmd: "reply" | "init" | "open" | "migrate" | "event" | "req";
|
cmd: "reply" | "init" | "open" | "migrate" | "event" | "req" | "count" | "summary";
|
||||||
args: T;
|
args: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,3 +27,7 @@ export interface ReqFilter {
|
|||||||
not?: ReqFilter;
|
not?: ReqFilter;
|
||||||
[key: string]: Array<string> | Array<number> | string | number | undefined | ReqFilter;
|
[key: string]: Array<string> | Array<number> | string | number | undefined | ReqFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unixNowMs() {
|
||||||
|
return new Date().getTime();
|
||||||
|
}
|
@ -13,43 +13,77 @@ async function reply<T>(id: string, obj?: T) {
|
|||||||
} as WorkerMessage<T>);
|
} as WorkerMessage<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event inserter queue
|
||||||
|
let eventWriteQueue: Array<NostrEvent> = [];
|
||||||
|
async function insertBatch() {
|
||||||
|
if (eventWriteQueue.length > 0) {
|
||||||
|
relay.eventBatch(eventWriteQueue);
|
||||||
|
eventWriteQueue = [];
|
||||||
|
}
|
||||||
|
setTimeout(() => insertBatch(), 100);
|
||||||
|
}
|
||||||
|
setTimeout(() => insertBatch(), 100);
|
||||||
|
|
||||||
|
globalThis.onclose = () => {
|
||||||
|
relay.close();
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.onmessage = async ev => {
|
globalThis.onmessage = async ev => {
|
||||||
//console.debug(ev);
|
//console.debug(ev);
|
||||||
|
|
||||||
const msg = ev.data as WorkerMessage<any>;
|
const msg = ev.data as WorkerMessage<any>;
|
||||||
switch (msg.cmd) {
|
try {
|
||||||
case "init": {
|
switch (msg.cmd) {
|
||||||
await relay.init();
|
case "init": {
|
||||||
reply(msg.id, true);
|
await relay.init();
|
||||||
break;
|
reply(msg.id, true);
|
||||||
}
|
break;
|
||||||
case "open": {
|
}
|
||||||
await relay.open("/relay.db");
|
case "open": {
|
||||||
reply(msg.id, true);
|
await relay.open("/relay.db");
|
||||||
break;
|
reply(msg.id, true);
|
||||||
}
|
break;
|
||||||
case "migrate": {
|
}
|
||||||
await relay.migrate();
|
case "migrate": {
|
||||||
reply(msg.id, true);
|
relay.migrate();
|
||||||
break;
|
reply(msg.id, true);
|
||||||
}
|
break;
|
||||||
case "event": {
|
}
|
||||||
await relay.event(msg.args as NostrEvent);
|
case "event": {
|
||||||
reply(msg.id, true);
|
eventWriteQueue.push(msg.args as NostrEvent);
|
||||||
break;
|
reply(msg.id, true);
|
||||||
}
|
break;
|
||||||
case "req": {
|
}
|
||||||
const req = msg.args as ReqCommand;
|
case "req": {
|
||||||
const results = [];
|
const req = msg.args as ReqCommand;
|
||||||
for (const r of req.slice(2)) {
|
const results = [];
|
||||||
results.push(...(await relay.req(r as ReqFilter)));
|
for (const r of req.slice(2)) {
|
||||||
|
results.push(...relay.req(r as ReqFilter));
|
||||||
|
}
|
||||||
|
reply(msg.id, results);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "count": {
|
||||||
|
const req = msg.args as ReqCommand;
|
||||||
|
let results = 0;
|
||||||
|
for (const r of req.slice(2)) {
|
||||||
|
const c = relay.count(r as ReqFilter);
|
||||||
|
results += c;
|
||||||
|
}
|
||||||
|
reply(msg.id, results);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "summary": {
|
||||||
|
const res = relay.summary();
|
||||||
|
reply(msg.id, res);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
reply(msg.id, { error: "Unknown command" });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
reply(msg.id, results);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
reply(msg.id, { error: "Unknown command" });
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reply(msg.id, { error: e });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2969,6 +2969,7 @@ __metadata:
|
|||||||
eslint-plugin-react-hooks: ^4.6.0
|
eslint-plugin-react-hooks: ^4.6.0
|
||||||
eslint-plugin-react-refresh: ^0.4.5
|
eslint-plugin-react-refresh: ^0.4.5
|
||||||
eslint-plugin-simple-import-sort: ^10.0.0
|
eslint-plugin-simple-import-sort: ^10.0.0
|
||||||
|
eventemitter3: ^5.0.1
|
||||||
fuse.js: ^7.0.0
|
fuse.js: ^7.0.0
|
||||||
highlight.js: ^11.8.0
|
highlight.js: ^11.8.0
|
||||||
light-bolt11-decoder: ^2.1.0
|
light-bolt11-decoder: ^2.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user