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()];
}
override async onEvent(evs: Array<TaggedNostrEvent>, pub?: EventPublisher) {
override async onEvent(evs: Array<TaggedNostrEvent>, _: string, pub?: EventPublisher) {
if (!pub) return;
const unwrapped = (

View File

@ -1,11 +1,11 @@
import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache";
import { LoginSession } from "Login";
import { db } from "Db";
import { NostrEventForSession, db } from "Db";
import { Day } from "Const";
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];
constructor() {
@ -14,7 +14,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
buildSub(session: LoginSession, rb: RequestBuilder) {
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()
.kinds(this.#kinds)
.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"));
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)));
}
}
@ -34,7 +39,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
return of.id;
}
takeSnapshot(): TWithCreated<NostrEvent>[] {
takeSnapshot() {
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>> {
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
*/
protected newest() {
protected newest(filter?: (e: TWithCreated<T>) => boolean) {
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,3 +90,22 @@ header {
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 { useNoteCreator } from "State/NoteCreator";
import { LoginUnlock } from "Element/PinPrompt";
import { LoginStore } from "Login";
export default function Layout() {
const location = useLocation();
const [pageClass, setPageClass] = useState("page");
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
useLoginFeed();
useTheme();
@ -60,6 +62,17 @@ export default function Layout() {
<Toaster />
</div>
<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();
};
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(() => {
return orderDescending([...notifications])
.filter(a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey))
.reduce((acc, v) => {
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
if (acc.has(key)) {
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
} else {
acc.set(key, [v as TaggedNostrEvent]);
}
return acc;
}, new Map<string, Array<TaggedNostrEvent>>());
}, [notifications]);
return myNotifications.reduce((acc, v) => {
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
if (acc.has(key)) {
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
} else {
acc.set(key, [v as TaggedNostrEvent]);
}
return acc;
}, new Map<string, Array<TaggedNostrEvent>>());
}, [myNotifications]);
return (
<div className="main-content">
<NotificationSummary evs={notifications as TaggedNostrEvent[]} />
<NotificationSummary evs={myNotifications as TaggedNostrEvent[]} />
{login.publicKey &&
[...timeGrouped.entries()].map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}