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()];
|
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 = (
|
||||||
|
@ -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()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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} />)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user