setup stalker mode
This commit is contained in:
parent
672255187f
commit
50bfd9eaa0
@ -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 = (
|
||||
|
@ -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()];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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} />)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user