setup stalker mode

This commit is contained in:
Kieran 2023-10-10 12:20:37 +01:00
parent 672255187f
commit 50bfd9eaa0
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
11 changed files with 88 additions and 31 deletions

View File

@ -24,7 +24,7 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
return [...this.cache.values()]; return [...this.cache.values()];
} }
override async onEvent(evs: Array<TaggedNostrEvent>, pub?: EventPublisher) { override async onEvent(evs: Array<TaggedNostrEvent>, _: string, pub?: EventPublisher) {
if (!pub) return; if (!pub) return;
const unwrapped = ( const unwrapped = (

View File

@ -1,11 +1,11 @@
import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system"; import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache"; import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache";
import { LoginSession } from "Login"; import { LoginSession } from "Login";
import { db } from "Db"; import { NostrEventForSession, db } from "Db";
import { Day } from "Const"; import { Day } from "Const";
import { unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
export class NotificationsCache extends RefreshFeedCache<NostrEvent> { export class NotificationsCache extends RefreshFeedCache<NostrEventForSession> {
#kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]; #kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
constructor() { constructor() {
@ -14,7 +14,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
buildSub(session: LoginSession, rb: RequestBuilder) { buildSub(session: LoginSession, rb: RequestBuilder) {
if (session.publicKey) { if (session.publicKey) {
const newest = this.newest(); const newest = this.newest(v => v.tags.some(a => a[0] === "p" && a[1] === session.publicKey));
rb.withFilter() rb.withFilter()
.kinds(this.#kinds) .kinds(this.#kinds)
.tag("p", [session.publicKey]) .tag("p", [session.publicKey])
@ -22,10 +22,15 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
} }
} }
async onEvent(evs: readonly TaggedNostrEvent[]) { async onEvent(evs: readonly TaggedNostrEvent[], pubKey: string) {
const filtered = evs.filter(a => this.#kinds.includes(a.kind) && a.tags.some(b => b[0] === "p")); const filtered = evs.filter(a => this.#kinds.includes(a.kind) && a.tags.some(b => b[0] === "p"));
if (filtered.length > 0) { if (filtered.length > 0) {
await this.bulkSet(filtered); await this.bulkSet(
filtered.map(v => ({
...v,
forSession: pubKey,
})),
);
this.notifyChange(filtered.map(v => this.key(v))); this.notifyChange(filtered.map(v => this.key(v)));
} }
} }
@ -34,7 +39,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
return of.id; return of.id;
} }
takeSnapshot(): TWithCreated<NostrEvent>[] { takeSnapshot() {
return [...this.cache.values()]; return [...this.cache.values()];
} }
} }

View File

@ -6,14 +6,18 @@ export type TWithCreated<T> = (T | Readonly<T>) & { created_at: number };
export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> { export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> {
abstract buildSub(session: LoginSession, rb: RequestBuilder): void; abstract buildSub(session: LoginSession, rb: RequestBuilder): void;
abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pub?: EventPublisher): void; abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pubKey: string, pub?: EventPublisher): void;
/** /**
* Get latest event * Get latest event
*/ */
protected newest() { protected newest(filter?: (e: TWithCreated<T>) => boolean) {
let ret = 0; let ret = 0;
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret)); this.cache.forEach(v => {
if (!filter || filter(v)) {
ret = v.created_at > ret ? v.created_at : ret;
}
});
return ret; return ret;
} }

View File

@ -2,7 +2,7 @@ import Dexie, { Table } from "dexie";
import { HexKey, NostrEvent, TaggedNostrEvent, u256 } from "@snort/system"; import { HexKey, NostrEvent, TaggedNostrEvent, u256 } from "@snort/system";
export const NAME = "snortDB"; export const NAME = "snortDB";
export const VERSION = 14; export const VERSION = 15;
export interface SubCache { export interface SubCache {
id: string; id: string;
@ -35,6 +35,10 @@ export interface UnwrappedGift {
tags?: Array<Array<string>>; // some tags extracted tags?: Array<Array<string>>; // some tags extracted
} }
export type NostrEventForSession = TaggedNostrEvent & {
forSession: string;
};
const STORES = { const STORES = {
chats: "++id", chats: "++id",
eventInteraction: "++id", eventInteraction: "++id",
@ -50,7 +54,7 @@ export class SnortDB extends Dexie {
eventInteraction!: Table<EventInteraction>; eventInteraction!: Table<EventInteraction>;
payments!: Table<Payment>; payments!: Table<Payment>;
gifts!: Table<UnwrappedGift>; gifts!: Table<UnwrappedGift>;
notifications!: Table<NostrEvent>; notifications!: Table<NostrEventForSession>;
followsFeed!: Table<TaggedNostrEvent>; followsFeed!: Table<TaggedNostrEvent>;
constructor() { constructor() {

View File

@ -129,8 +129,10 @@ export default function ProfileImage({
<div className="flex f-space"> <div className="flex f-space">
<ProfileImage pubkey={""} profile={user} showProfileCard={false} link="" /> <ProfileImage pubkey={""} profile={user} showProfileCard={false} link="" />
<div className="flex g8"> <div className="flex g8">
{/*<button type="button"> {/*<button type="button" onClick={() => {
<FormattedMessage defaultMessage="Stalk" /> LoginStore.loginWithPubkey(pubkey, LoginSessionType.PublicKey, undefined, undefined, undefined, true);
}}>
<FormattedMessage defaultMessage="Stalk" />
</button>*/} </button>*/}
<FollowButton pubkey={pubkey} /> <FollowButton pubkey={pubkey} />
</div> </div>

View File

@ -5,6 +5,7 @@ import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { RefreshFeedCache } from "Cache/RefreshFeedCache"; import { RefreshFeedCache } from "Cache/RefreshFeedCache";
import useLogin from "./useLogin"; import useLogin from "./useLogin";
import useEventPublisher from "./useEventPublisher"; import useEventPublisher from "./useEventPublisher";
import { unwrap } from "@snort/shared";
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) { export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
const system = useContext(SnortContext); const system = useContext(SnortContext);
@ -33,7 +34,7 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
tBuf = [...evs]; tBuf = [...evs];
t = setTimeout(() => { t = setTimeout(() => {
t = undefined; t = undefined;
c.onEvent(tBuf, publisher); c.onEvent(tBuf, unwrap(login.publicKey), publisher);
}, 100); }, 100);
} else { } else {
tBuf.push(...evs); tBuf.push(...evs);
@ -46,8 +47,5 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
releaseOnEvent(); releaseOnEvent();
}; };
} }
return () => {
// noop
};
}, [sub]); }, [sub]);
} }

View File

@ -128,4 +128,9 @@ export interface LoginSession {
* A list of chats which we have joined (NIP-28/NIP-29) * A list of chats which we have joined (NIP-28/NIP-29)
*/ */
extraChats: Array<string>; extraChats: Array<string>;
/**
* Is login session in stalker mode
*/
stalker: boolean;
} }

View File

@ -53,6 +53,7 @@ const LoggedOut = {
timestamp: 0, timestamp: 0,
}, },
extraChats: [], extraChats: [],
stalker: false,
} as LoginSession; } as LoginSession;
export class MultiAccountStore extends ExternalStore<LoginSession> { export class MultiAccountStore extends ExternalStore<LoginSession> {
@ -125,6 +126,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
relays?: Record<string, RelaySettings>, relays?: Record<string, RelaySettings>,
remoteSignerRelays?: Array<string>, remoteSignerRelays?: Array<string>,
privateKey?: KeyStorage, privateKey?: KeyStorage,
stalker?: boolean,
) { ) {
if (this.#accounts.has(key)) { if (this.#accounts.has(key)) {
throw new Error("Already logged in with this pubkey"); throw new Error("Already logged in with this pubkey");
@ -143,6 +145,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
preferences: deepClone(DefaultPreferences), preferences: deepClone(DefaultPreferences),
remoteSignerRelays, remoteSignerRelays,
privateKeyData: privateKey, privateKeyData: privateKey,
stalker: stalker ?? false,
} as LoginSession; } as LoginSession;
const pub = createPublisher(newSession); const pub = createPublisher(newSession);

View File

@ -90,3 +90,22 @@ header {
display: none; display: none;
} }
} }
.stalker {
position: fixed;
top: 0;
width: 100vw;
height: 100vh;
box-shadow: 0px 0px 26px 0px rgba(139, 92, 246, 0.7) inset;
pointer-events: none;
}
.stalker button {
position: absolute;
top: 50px;
right: 50px;
color: black;
background-color: var(--btn-color);
padding: 12px;
pointer-events: all !important;
}

View File

@ -22,10 +22,12 @@ import { useTheme } from "Hooks/useTheme";
import { useLoginRelays } from "Hooks/useLoginRelays"; import { useLoginRelays } from "Hooks/useLoginRelays";
import { useNoteCreator } from "State/NoteCreator"; import { useNoteCreator } from "State/NoteCreator";
import { LoginUnlock } from "Element/PinPrompt"; import { LoginUnlock } from "Element/PinPrompt";
import { LoginStore } from "Login";
export default function Layout() { export default function Layout() {
const location = useLocation(); const location = useLocation();
const [pageClass, setPageClass] = useState("page"); const [pageClass, setPageClass] = useState("page");
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
useLoginFeed(); useLoginFeed();
useTheme(); useTheme();
@ -60,6 +62,17 @@ export default function Layout() {
<Toaster /> <Toaster />
</div> </div>
<LoginUnlock /> <LoginUnlock />
{stalker && (
<div
className="stalker"
onClick={() => {
LoginStore.removeSession(id);
}}>
<button type="button" className="btn btn-rnd">
<Icon name="close" />
</button>
</div>
)}
</> </>
); );
} }

View File

@ -80,23 +80,27 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
return onHour.toString(); return onHour.toString();
}; };
const myNotifications = useMemo(() => {
return orderDescending([...notifications]).filter(
a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey),
);
}, [notifications, login.publicKey]);
const timeGrouped = useMemo(() => { const timeGrouped = useMemo(() => {
return orderDescending([...notifications]) return myNotifications.reduce((acc, v) => {
.filter(a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey)) const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
.reduce((acc, v) => { if (acc.has(key)) {
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`; unwrap(acc.get(key)).push(v as TaggedNostrEvent);
if (acc.has(key)) { } else {
unwrap(acc.get(key)).push(v as TaggedNostrEvent); acc.set(key, [v as TaggedNostrEvent]);
} else { }
acc.set(key, [v as TaggedNostrEvent]); return acc;
} }, new Map<string, Array<TaggedNostrEvent>>());
return acc; }, [myNotifications]);
}, new Map<string, Array<TaggedNostrEvent>>());
}, [notifications]);
return ( return (
<div className="main-content"> <div className="main-content">
<NotificationSummary evs={notifications as TaggedNostrEvent[]} /> <NotificationSummary evs={myNotifications as TaggedNostrEvent[]} />
{login.publicKey && {login.publicKey &&
[...timeGrouped.entries()].map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)} [...timeGrouped.entries()].map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}