Merge pull request 'feat: gossip model' (#567) from gossip-model into main
continuous-integration/drone/push Build is failing Details

Reviewed-on: #567
This commit is contained in:
Kieran 2023-06-13 09:09:22 +00:00
commit f1e089b0a1
198 changed files with 4555 additions and 2236 deletions

3
.gitignore vendored
View File

@ -2,3 +2,6 @@ node_modules/
.idea
.yarn
yarn.lock
dist/
*.tgz
*.log

View File

@ -4,8 +4,9 @@
"packages/*"
],
"scripts": {
"build": "yarn workspace @snort/nostr build && yarn workspace @snort/app build",
"start": "yarn workspace @snort/nostr build && yarn workspace @snort/app start"
"build": "yarn workspace @snort/system build && yarn workspace @snort/app build",
"start": "yarn workspace @snort/system build && yarn workspace @snort/app start",
"test": "yarn workspace @snort/app test"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.3",

View File

@ -23,3 +23,5 @@ yarn-debug.log*
yarn-error.log*
.idea
dist/

View File

@ -0,0 +1,9 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
bail: true,
preset: "ts-jest",
testEnvironment: "jsdom",
roots: ["src"],
moduleDirectories: ["src", "node_modules"],
setupFiles: ["./src/setupTests.ts"],
};

View File

@ -12,11 +12,11 @@
"@reduxjs/toolkit": "^1.9.1",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.1.1",
"@snort/nostr": "^1.0.0",
"@szhsin/react-menu": "^3.3.1",
"@void-cat/api": "^1.0.4",
"base32-decode": "^1.0.0",
"bech32": "^2.0.0",
"debug": "^4.3.4",
"dexie": "^3.2.2",
"dexie-react-hooks": "^1.1.1",
"dns-over-http-resolver": "^2.1.1",
@ -48,7 +48,7 @@
"scripts": {
"start": "webpack serve",
"build": "webpack --node-env=production",
"test": "",
"test": "jest --runInBand",
"intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true",
"intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json",
"format": "prettier --write .",
@ -80,7 +80,8 @@
"@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^6.0.1",
"@formatjs/ts-transformer": "^3.13.1",
"@types/jest": "^29.2.5",
"@types/debug": "^4.1.8",
"@types/jest": "^29.5.1",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
@ -99,9 +100,12 @@
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.1",
"husky": ">=6",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"lint-staged": ">=10",
"mini-css-extract-plugin": "^2.7.5",
"prettier": "2.8.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"webpack": "^5.82.1",

View File

@ -1,13 +1,13 @@
import { RawEvent } from "@snort/nostr";
import { NostrEvent } from "@snort/system";
import { db } from "Db";
import FeedCache from "./FeedCache";
class DMCache extends FeedCache<RawEvent> {
class DMCache extends FeedCache<NostrEvent> {
constructor() {
super("DMCache", db.dms);
}
key(of: RawEvent): string {
key(of: NostrEvent): string {
return of.id;
}
@ -23,11 +23,11 @@ class DMCache extends FeedCache<RawEvent> {
return ret;
}
allDms(): Array<RawEvent> {
allDms(): Array<NostrEvent> {
return [...this.cache.values()];
}
takeSnapshot(): Array<RawEvent> {
takeSnapshot(): Array<NostrEvent> {
return this.allDms();
}
}

View File

@ -1,6 +1,6 @@
import { db, EventInteraction } from "Db";
import { LoginStore } from "Login";
import { sha256 } from "Util";
import { sha256 } from "SnortUtils";
import FeedCache from "./FeedCache";
class EventInteractionCache extends FeedCache<EventInteraction> {

View File

@ -1,6 +1,7 @@
import { db } from "Db";
import debug from "debug";
import { Table } from "dexie";
import { unixNowMs, unwrap } from "Util";
import { unixNowMs, unwrap } from "SnortUtils";
type HookFn = () => void;
@ -11,31 +12,32 @@ interface HookFilter {
export default abstract class FeedCache<TCached> {
#name: string;
#table: Table<TCached>;
#hooks: Array<HookFilter> = [];
#snapshot: Readonly<Array<TCached>> = [];
#changed = true;
#hits = 0;
#miss = 0;
protected table: Table<TCached>;
protected onTable: Set<string> = new Set();
protected cache: Map<string, TCached> = new Map();
constructor(name: string, table: Table<TCached>) {
this.#name = name;
this.#table = table;
this.table = table;
setInterval(() => {
console.debug(
`[${this.#name}] ${this.cache.size} loaded, ${this.onTable.size} on-disk, ${this.#hooks.length} hooks, ${(
(this.#hits / (this.#hits + this.#miss)) *
100
).toFixed(1)} % hit`
debug(this.#name)(
"%d loaded, %d on-disk, %d hooks, %d% hit",
this.cache.size,
this.onTable.size,
this.#hooks.length,
((this.#hits / (this.#hits + this.#miss)) * 100).toFixed(1)
);
}, 5_000);
}, 30_000);
}
async preload() {
if (db.ready) {
const keys = await this.#table.toCollection().primaryKeys();
const keys = await this.table.toCollection().primaryKeys();
this.onTable = new Set<string>(keys.map(a => a as string));
}
}
@ -73,7 +75,7 @@ export default abstract class FeedCache<TCached> {
async get(key?: string) {
if (key && !this.cache.has(key) && db.ready) {
const cached = await this.#table.get(key);
const cached = await this.table.get(key);
if (cached) {
this.cache.set(this.key(cached), cached);
this.notifyChange([key]);
@ -86,7 +88,7 @@ export default abstract class FeedCache<TCached> {
async bulkGet(keys: Array<string>) {
const missing = keys.filter(a => !this.cache.has(a));
if (missing.length > 0 && db.ready) {
const cached = await this.#table.bulkGet(missing);
const cached = await this.table.bulkGet(missing);
cached.forEach(a => {
if (a) {
this.cache.set(this.key(a), a);
@ -103,7 +105,7 @@ export default abstract class FeedCache<TCached> {
const k = this.key(obj);
this.cache.set(k, obj);
if (db.ready) {
await this.#table.put(obj);
await this.table.put(obj);
this.onTable.add(k);
}
this.notifyChange([k]);
@ -111,13 +113,44 @@ export default abstract class FeedCache<TCached> {
async bulkSet(obj: Array<TCached>) {
if (db.ready) {
await this.#table.bulkPut(obj);
await this.table.bulkPut(obj);
obj.forEach(a => this.onTable.add(this.key(a)));
}
obj.forEach(v => this.cache.set(this.key(v), v));
this.notifyChange(obj.map(a => this.key(a)));
}
/**
* Try to update an entry where created values exists
* @param m Profile metadata
* @returns
*/
async update<TCachedWithCreated extends TCached & { created: number; loaded: number }>(m: TCachedWithCreated) {
const k = this.key(m);
const existing = this.getFromCache(k) as TCachedWithCreated;
const updateType = (() => {
if (!existing) {
return "new";
}
if (existing.created < m.created) {
return "updated";
}
if (existing && existing.loaded < m.loaded) {
return "refresh";
}
return "no_change";
})();
debug(this.#name)("Updating %s %s %o", k, updateType, m);
if (updateType !== "no_change") {
const updated = {
...existing,
...m,
};
await this.set(updated);
}
return updateType;
}
/**
* Loads a list of rows from disk cache
* @param keys List of ids to load
@ -131,16 +164,17 @@ export default abstract class FeedCache<TCached> {
key: a,
}));
const start = unixNowMs();
const fromCache = await this.#table.bulkGet(mapped.filter(a => a.has).map(a => a.key));
const fromCache = await this.table.bulkGet(mapped.filter(a => a.has).map(a => a.key));
const fromCacheFiltered = fromCache.filter(a => a !== undefined).map(a => unwrap(a));
fromCacheFiltered.forEach(a => {
this.cache.set(this.key(a), a);
});
this.notifyChange(fromCacheFiltered.map(a => this.key(a)));
console.debug(
`[${this.#name}] Loaded ${fromCacheFiltered.length}/${keys.length} in ${(
unixNowMs() - start
).toLocaleString()} ms`
debug(this.#name)(
`Loaded %d/%d in %d ms`,
fromCacheFiltered.length,
keys.length,
(unixNowMs() - start).toLocaleString()
);
return mapped.filter(a => !a.has).map(a => a.key);
}
@ -150,7 +184,7 @@ export default abstract class FeedCache<TCached> {
}
async clear() {
await this.#table.clear();
await this.table.clear();
this.cache.clear();
this.onTable.clear();
}

View File

@ -1,6 +1,6 @@
import FeedCache from "Cache/FeedCache";
import { db } from "Db";
import { MetadataCache } from "Cache";
import { MetadataCache } from "@snort/system";
import { LNURL } from "LNURL";
import { fetchNip05Pubkey } from "Nip05/Verifier";
@ -18,6 +18,14 @@ class UserProfileCache extends FeedCache<MetadataCache> {
return of.pubkey;
}
override async preload(follows?: Array<string>): Promise<void> {
await super.preload();
// load follows profiles
if (follows) {
await this.buffer(follows);
}
}
async search(q: string): Promise<Array<MetadataCache>> {
if (db.ready) {
// on-disk cache will always have more data
@ -53,35 +61,15 @@ class UserProfileCache extends FeedCache<MetadataCache> {
* @param m Profile metadata
* @returns
*/
async update(m: MetadataCache) {
const existing = this.getFromCache(m.pubkey);
const updateType = (() => {
if (!existing) {
return "new_profile";
}
if (existing.created < m.created) {
return "updated_profile";
}
if (existing && existing.loaded < m.loaded) {
return "refresh_profile";
}
return "no_change";
})();
console.debug(`Updating ${m.pubkey} ${updateType}`, m);
if (updateType !== "no_change") {
const writeProfile = {
...existing,
...m,
};
await this.#setItem(writeProfile);
if (updateType !== "refresh_profile") {
const lnurl = m.lud16 ?? m.lud06;
if (lnurl) {
this.#zapperQueue.push({
pubkey: m.pubkey,
lnurl,
});
}
override async update(m: MetadataCache) {
const updateType = await super.update(m);
if (updateType !== "refresh") {
const lnurl = m.lud16 ?? m.lud06;
if (lnurl) {
this.#zapperQueue.push({
pubkey: m.pubkey,
lnurl,
});
}
if (m.nip05) {
this.#nip5Queue.push({
@ -97,15 +85,6 @@ class UserProfileCache extends FeedCache<MetadataCache> {
return [];
}
async #setItem(m: MetadataCache) {
this.cache.set(m.pubkey, m);
if (db.ready) {
await db.users.put(m);
this.onTable.add(m.pubkey);
}
this.notifyChange([m.pubkey]);
}
async #processZapperQueue() {
await this.#batchQueue(
this.#zapperQueue,
@ -114,7 +93,7 @@ class UserProfileCache extends FeedCache<MetadataCache> {
await svc.load();
const p = this.getFromCache(i.pubkey);
if (p) {
this.#setItem({
await this.set({
...p,
zapService: svc.zapperPubkey,
});
@ -134,7 +113,7 @@ class UserProfileCache extends FeedCache<MetadataCache> {
const nip5pk = await fetchNip05Pubkey(name, domain);
const p = this.getFromCache(i.pubkey);
if (p) {
this.#setItem({
await this.set({
...p,
isNostrAddressValid: i.pubkey === nip5pk,
});

View File

@ -0,0 +1,31 @@
import { db, UsersRelays } from "Db";
import FeedCache from "./FeedCache";
export class UsersRelaysCache extends FeedCache<UsersRelays> {
constructor() {
super("UserRelays", db.userRelays);
}
key(of: UsersRelays): string {
return of.pubkey;
}
override async preload(follows?: Array<string>): Promise<void> {
await super.preload();
if (follows) {
await this.buffer(follows);
}
}
newest(): number {
let ret = 0;
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
return ret;
}
takeSnapshot(): Array<UsersRelays> {
return [...this.cache.values()];
}
}
export const UserRelays = new UsersRelaysCache();

View File

@ -1,60 +1,16 @@
import { HexKey, RawEvent, UserMetadata } from "@snort/nostr";
import { hexToBech32, unixNowMs } from "Util";
import { DmCache } from "./DMCache";
import { InteractionCache } from "./EventInteractionCache";
import { UserCache } from "./UserCache";
import { UserRelays } from "./UserRelayCache";
export interface MetadataCache extends UserMetadata {
/**
* When the object was saved in cache
*/
loaded: number;
/**
* When the source metadata event was created
*/
created: number;
/**
* The pubkey of the owner of this metadata
*/
pubkey: HexKey;
/**
* The bech32 encoded pubkey
*/
npub: string;
/**
* Pubkey of zapper service
*/
zapService?: HexKey;
/**
* If the nip05 is valid for this user
*/
isNostrAddressValid: boolean;
}
export function mapEventToProfile(ev: RawEvent) {
try {
const data: UserMetadata = JSON.parse(ev.content);
return {
pubkey: ev.pubkey,
npub: hexToBech32("npub", ev.pubkey),
created: ev.created_at,
...data,
loaded: unixNowMs(),
} as MetadataCache;
} catch (e) {
console.error("Failed to parse JSON", ev, e);
}
}
export async function preload() {
await UserCache.preload();
await DmCache.preload();
await InteractionCache.preload();
export async function preload(follows?: Array<string>) {
const preloads = [
UserCache.preload(follows),
DmCache.preload(),
InteractionCache.preload(),
UserRelays.preload(follows),
];
await Promise.all(preloads);
}
export { UserCache, DmCache };

View File

@ -1,4 +1,4 @@
import { RelaySettings } from "@snort/nostr";
import { RelaySettings } from "@snort/system";
/**
* Add-on api for snort features
@ -33,7 +33,7 @@ export const DefaultConnectTimeout = 2000;
/**
* How long profile cache should be considered valid for
*/
export const ProfileCacheExpire = 1_000 * 60 * 30;
export const ProfileCacheExpire = 1_000 * 60 * 60 * 6;
/**
* Default bootstrap relays

View File

@ -1,6 +1,5 @@
import Dexie, { Table } from "dexie";
import { FullRelaySettings, HexKey, RawEvent, u256 } from "@snort/nostr";
import { MetadataCache } from "Cache";
import { FullRelaySettings, HexKey, NostrEvent, u256, MetadataCache } from "@snort/system";
export const NAME = "snortDB";
export const VERSION = 9;
@ -21,6 +20,7 @@ export interface RelayMetrics {
export interface UsersRelays {
pubkey: HexKey;
created_at: number;
relays: FullRelaySettings[];
}
@ -55,8 +55,8 @@ export class SnortDB extends Dexie {
users!: Table<MetadataCache>;
relayMetrics!: Table<RelayMetrics>;
userRelays!: Table<UsersRelays>;
events!: Table<RawEvent>;
dms!: Table<RawEvent>;
events!: Table<NostrEvent>;
dms!: Table<NostrEvent>;
eventInteraction!: Table<EventInteraction>;
payments!: Table<Payment>;

View File

@ -2,7 +2,7 @@ import "./Avatar.css";
import Nostrich from "nostrich.webp";
import { CSSProperties, useEffect, useState } from "react";
import type { UserMetadata } from "@snort/nostr";
import type { UserMetadata } from "@snort/system";
import useImgProxy from "Hooks/useImgProxy";

View File

@ -1,7 +1,7 @@
import Icon from "Icons/Icon";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile, unwrap } from "Util";
import { openFile, unwrap } from "SnortUtils";
interface AvatarEditorProps {
picture?: string;

View File

@ -3,13 +3,13 @@ import "./BadgeList.css";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import Icon from "Icons/Icon";
import Modal from "Element/Modal";
import Username from "Element/Username";
import { findTag } from "Util";
import { findTag } from "SnortUtils";
export default function BadgeList({ badges }: { badges: TaggedRawEvent[] }) {
const [showModal, setShowModal] = useState(false);

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import { useState, useMemo, ChangeEvent } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey, TaggedRawEvent } from "@snort/nostr";
import { HexKey, TaggedRawEvent } from "@snort/system";
import Note from "Element/Note";
import useLogin from "Hooks/useLogin";

View File

@ -2,13 +2,13 @@ import "./DM.css";
import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import NoteTime from "Element/NoteTime";
import Text from "Element/Text";
import { setLastReadDm } from "Pages/MessagesPage";
import { unwrap } from "Util";
import { unwrap } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import "./DmWindow.css";
import { useEffect, useMemo, useRef } from "react";
import { TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/system";
import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM";
@ -9,7 +9,7 @@ import NoteToSelf from "Element/NoteToSelf";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
import WriteDm from "Element/WriteDm";
import { unwrap } from "Util";
import { unwrap } from "SnortUtils";
export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey;

View File

@ -1,9 +1,9 @@
import "./FollowButton.css";
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import { parseId } from "Util";
import { parseId } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import AsyncButton from "Element/AsyncButton";

View File

@ -1,6 +1,6 @@
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import ProfilePreview from "Element/ProfilePreview";

View File

@ -13,7 +13,7 @@ import {
NostrNestsRegex,
WavlakeRegex,
} from "Const";
import { magnetURIDecode } from "Util";
import { magnetURIDecode } from "SnortUtils";
import SoundCloudEmbed from "Element/SoundCloudEmded";
import MixCloudEmbed from "Element/MixCloudEmbed";
import SpotifyEmbed from "Element/SpotifyEmbed";

View File

@ -6,7 +6,7 @@ import { useMemo } from "react";
import SendSats from "Element/SendSats";
import Icon from "Icons/Icon";
import { useWallet } from "Wallet";
import { decodeInvoice } from "Util";
import { decodeInvoice } from "SnortUtils";
import messages from "./messages";

View File

@ -1,6 +1,6 @@
import { FormattedMessage } from "react-intl";
import { Magnet } from "Util";
import { Magnet } from "SnortUtils";
interface MagnetLinkProps {
magnet: Magnet;

View File

@ -1,9 +1,9 @@
import { useMemo } from "react";
import { Link } from "react-router-dom";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "Util";
import { profileLink } from "SnortUtils";
import { getDisplayName } from "Element/ProfileImage";
export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array<string> | string }) {

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import useModeration from "Hooks/useModeration";
import messages from "./messages";

View File

@ -1,5 +1,5 @@
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import MuteButton from "Element/MuteButton";
import ProfilePreview from "Element/ProfilePreview";
import useModeration from "Hooks/useModeration";

View File

@ -1,5 +1,5 @@
import "./Nip05.css";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import Icon from "Icons/Icon";
import { useUserProfile } from "Hooks/useUserProfile";

View File

@ -1,9 +1,9 @@
import { useEffect, useMemo, useState, ChangeEvent } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { UserMetadata } from "@snort/nostr";
import { UserMetadata, mapEventToProfile } from "@snort/system";
import { unwrap } from "Util";
import { unwrap } from "SnortUtils";
import { formatShort } from "Number";
import {
ServiceProvider,
@ -19,10 +19,10 @@ import SendSats from "Element/SendSats";
import Copy from "Element/Copy";
import { useUserProfile } from "Hooks/useUserProfile";
import useEventPublisher from "Feed/EventPublisher";
import { debounce } from "Util";
import { debounce } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import SnortServiceProvider from "Nip05/SnortServiceProvider";
import { mapEventToProfile, UserCache } from "Cache";
import { UserCache } from "Cache";
import messages from "./messages";

View File

@ -1,7 +1,7 @@
import { FormattedMessage } from "react-intl";
import { RawEvent } from "@snort/nostr";
import { NostrEvent, NostrLink } from "@snort/system";
import { findTag, NostrLink } from "Util";
import { findTag } from "SnortUtils";
import useEventFeed from "Feed/EventFeed";
import PageSpinner from "Element/PageSpinner";
import Reveal from "Element/Reveal";
@ -14,7 +14,7 @@ export default function NostrFileHeader({ link }: { link: NostrLink }) {
return <NostrFileElement ev={ev.data} />;
}
export function NostrFileElement({ ev }: { ev: RawEvent }) {
export function NostrFileElement({ ev }: { ev: NostrEvent }) {
// assume image or embed which can be rendered by the hypertext kind
// todo: make use of hash
// todo: use magnet or other links if present

View File

@ -1,8 +1,7 @@
import { Link } from "react-router-dom";
import { NostrPrefix } from "@snort/nostr";
import { NostrPrefix, parseNostrLink } from "@snort/system";
import Mention from "Element/Mention";
import { parseNostrLink } from "Util";
import NoteQuote from "Element/NoteQuote";
export default function NostrLink({ link, depth }: { link: string; depth?: number }) {

View File

@ -3,7 +3,7 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists } from "@snort/nostr";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
@ -19,14 +19,13 @@ import {
normalizeReaction,
Reaction,
profileLink,
} from "Util";
} from "SnortUtils";
import NoteFooter, { Translation } from "Element/NoteFooter";
import NoteTime from "Element/NoteTime";
import Reveal from "Element/Reveal";
import useModeration from "Hooks/useModeration";
import { UserCache } from "Cache/UserCache";
import Poll from "Element/Poll";
import { EventExt } from "System/EventExt";
import useLogin from "Hooks/useLogin";
import { setBookmarked, setPinned } from "Login";
import { NostrFileElement } from "Element/NostrFileHeader";

View File

@ -1,11 +1,11 @@
import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent } from "@snort/nostr";
import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent, EventBuilder } from "@snort/system";
import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
import { openFile } from "Util";
import { openFile } from "SnortUtils";
import Textarea from "Element/Textarea";
import Modal from "Element/Modal";
import ProfileImage from "Element/ProfileImage";
@ -31,7 +31,6 @@ import { LNURL } from "LNURL";
import messages from "./messages";
import { ClipboardEventHandler, useState } from "react";
import Spinner from "Icons/Spinner";
import { EventBuilder } from "System";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { LoginStore } from "Login";
import { getCurrentSubscription } from "Subscription";

View File

@ -3,14 +3,14 @@ import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/nostr";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/system";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
import { delay, normalizeReaction, unwrap } from "Util";
import { delay, normalizeReaction, unwrap } from "SnortUtils";
import { NoteCreator } from "Element/NoteCreator";
import { ReBroadcaster } from "Element/ReBroadcaster";
import Reactions from "Element/Reactions";

View File

@ -1,5 +1,5 @@
import useEventFeed from "Feed/EventFeed";
import { NostrLink } from "Util";
import { NostrLink } from "@snort/system";
import Note from "Element/Note";
import PageSpinner from "Element/PageSpinner";

View File

@ -1,14 +1,13 @@
import "./NoteReaction.css";
import { Link } from "react-router-dom";
import { useMemo } from "react";
import { EventKind, RawEvent, TaggedRawEvent, NostrPrefix } from "@snort/nostr";
import { EventKind, NostrEvent, TaggedRawEvent, NostrPrefix, EventExt } from "@snort/system";
import Note from "Element/Note";
import ProfileImage from "Element/ProfileImage";
import { eventLink, hexToBech32 } from "Util";
import { eventLink, hexToBech32 } from "SnortUtils";
import NoteTime from "Element/NoteTime";
import useModeration from "Hooks/useModeration";
import { EventExt } from "System/EventExt";
export interface NoteReactionProps {
data: TaggedRawEvent;
@ -43,7 +42,7 @@ export default function NoteReaction(props: NoteReactionProps) {
function extractRoot() {
if (ev?.kind === EventKind.Repost && ev.content.length > 0 && ev.content !== "#[0]") {
try {
const r: RawEvent = JSON.parse(ev.content);
const r: NostrEvent = JSON.parse(ev.content);
return r as TaggedRawEvent;
} catch (e) {
console.error("Could not load reposted content", e);

View File

@ -1,7 +1,7 @@
import "./NoteToSelf.css";
import { Link, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { profileLink } from "Util";
import { profileLink } from "SnortUtils";
import messages from "./messages";
import Icon from "Icons/Icon";

View File

@ -1,4 +1,4 @@
import { TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/system";
import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@ -8,7 +8,7 @@ import useEventPublisher from "Feed/EventPublisher";
import { useWallet } from "Wallet";
import { useUserProfile } from "Hooks/useUserProfile";
import { LNURL } from "LNURL";
import { unwrap } from "Util";
import { unwrap } from "SnortUtils";
import { formatShort } from "Number";
import Spinner from "Icons/Spinner";
import SendSats from "Element/SendSats";

View File

@ -1,13 +1,12 @@
import "./ProfileImage.css";
import React, { useMemo } from "react";
import { HexKey, NostrPrefix } from "@snort/nostr";
import { HexKey, NostrPrefix, MetadataCache } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { hexToBech32, profileLink } from "Util";
import { hexToBech32, profileLink } from "SnortUtils";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
import { MetadataCache } from "Cache";
import { Link } from "react-router-dom";
export interface ProfileImageProps {

View File

@ -4,7 +4,7 @@ import { ReactNode } from "react";
import ProfileImage from "Element/ProfileImage";
import FollowButton from "Element/FollowButton";
import { useUserProfile } from "Hooks/useUserProfile";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { useInView } from "react-intersection-observer";
export interface ProfilePreviewProps {

View File

@ -1,7 +1,7 @@
import useImgProxy from "Hooks/useImgProxy";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { getUrlHostname } from "Util";
import { getUrlHostname } from "SnortUtils";
interface ProxyImgProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
size?: number;

View File

@ -1,7 +1,7 @@
import { RawEvent } from "@snort/nostr";
import { NostrEvent } from "@snort/system";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { dedupe, hexToBech32, unixNow } from "Util";
import { dedupe, hexToBech32, unixNow } from "SnortUtils";
import FollowListBase from "Element/FollowListBase";
import AsyncButton from "Element/AsyncButton";
import { useWallet } from "Wallet";
@ -13,7 +13,7 @@ import { LNURL } from "LNURL";
import useEventPublisher from "Feed/EventPublisher";
import { WalletInvoiceState } from "Wallet";
export default function PubkeyList({ ev, className }: { ev: RawEvent; className?: string }) {
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
const wallet = useWallet();
const login = useLogin();
const publisher = useEventPublisher();

View File

@ -2,7 +2,7 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/system";
import { formatShort } from "Number";
import Icon from "Icons/Icon";

View File

@ -2,11 +2,11 @@ import "./Relay.css";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { RelaySettings } from "@snort/nostr";
import { RelaySettings } from "@snort/system";
import useRelayState from "Feed/RelayState";
import { System } from "System";
import { getRelayName, unixNowMs, unwrap } from "Util";
import { System } from "index";
import { getRelayName, unixNowMs, unwrap } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import { setRelays } from "Login";
import Icon from "Icons/Icon";
@ -20,7 +20,9 @@ export interface RelayProps {
export default function Relay(props: RelayProps) {
const navigate = useNavigate();
const login = useLogin();
const relaySettings = unwrap(login.relays.item[props.addr] ?? System.Sockets.get(props.addr)?.Settings ?? {});
const relaySettings = unwrap(
login.relays.item[props.addr] ?? System.Sockets.find(a => a.address === props.addr)?.settings ?? {}
);
const state = useRelayState(props.addr);
const name = useMemo(() => getRelayName(props.addr), [props.addr]);
@ -35,7 +37,6 @@ export default function Relay(props: RelayProps) {
);
}
const latency = Math.floor(state?.avgLatency ?? 0);
return (
<>
<div className={`relay w-max`}>

View File

@ -2,7 +2,7 @@ import "./RelaysMetadata.css";
import Nostrich from "nostrich.webp";
import { useState } from "react";
import { FullRelaySettings } from "@snort/nostr";
import { FullRelaySettings } from "@snort/system";
import Icon from "Icons/Icon";
const RelayFavicon = ({ url }: { url: string }) => {

View File

@ -1,8 +1,9 @@
import "./SendSats.css";
import React, { useEffect, useMemo, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey, RawEvent } from "@snort/nostr";
import { HexKey, NostrEvent, EventPublisher } from "@snort/system";
import { System } from "index";
import { formatShort } from "Number";
import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
@ -11,11 +12,10 @@ import Modal from "Element/Modal";
import QrCode from "Element/QrCode";
import Copy from "Element/Copy";
import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } from "LNURL";
import { chunks, debounce } from "Util";
import { chunks, debounce } from "SnortUtils";
import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
import { generateRandomKey } from "Login";
import { EventPublisher } from "System/EventPublisher";
import { ZapPoolController } from "ZapPoolController";
import messages from "./messages";
@ -124,7 +124,7 @@ export default function SendSats(props: SendSatsProps) {
async function loadInvoice() {
if (!amount || !handler || !publisher) return null;
let zap: RawEvent | undefined;
let zap: NostrEvent | undefined;
if (author && zapType !== ZapType.NonZap) {
const relays = Object.keys(login.relays.item);
@ -133,7 +133,7 @@ export default function SendSats(props: SendSatsProps) {
const randomKey = generateRandomKey();
console.debug("Generated new key for zap: ", randomKey);
const publisher = new EventPublisher(randomKey.publicKey, randomKey.privateKey);
const publisher = new EventPublisher(System, randomKey.publicKey, randomKey.privateKey);
zap = await publisher.zap(amount * 1000, author, relays, note, comment, eb => eb.tag(["anon", ""]));
} else {
zap = await publisher.zap(amount * 1000, author, relays, note, comment);

View File

@ -3,11 +3,11 @@ import { useState } from "react";
import useRelayState from "Feed/RelayState";
import Tabs, { Tab } from "Element/Tabs";
import { System } from "System";
import { unwrap } from "Util";
import { unwrap } from "SnortUtils";
import useSystemState from "Hooks/useSystemState";
import { RawReqFilter } from "@snort/nostr";
import { ReqFilter } from "@snort/system";
import { useCopy } from "useCopy";
import { System } from "index";
function RelayInfo({ id }: { id: string }) {
const state = useRelayState(id);
@ -18,7 +18,7 @@ function Queries() {
const qs = useSystemState();
const { copy } = useCopy();
function countElements(filters: Array<RawReqFilter>) {
function countElements(filters: Array<ReqFilter>) {
let total = 0;
for (const f of filters) {
for (const v of Object.values(f)) {
@ -30,15 +30,10 @@ function Queries() {
return total;
}
function queryInfo(q: {
id: string;
filters: Array<RawReqFilter>;
closing: boolean;
subFilters: Array<RawReqFilter>;
}) {
function queryInfo(q: { id: string; filters: Array<ReqFilter>; subFilters: Array<ReqFilter> }) {
return (
<div key={q.id}>
{q.closing ? <s>{q.id}</s> : <>{q.id}</>}
{q.id}
<br />
<span onClick={() => copy(JSON.stringify(q.filters))} className="pointer">
&nbsp; Filters: {q.filters.length} ({countElements(q.filters)} elements)
@ -66,8 +61,8 @@ const SubDebug = () => {
return (
<>
<b>Connections:</b>
{[...System.Sockets.keys()].map(k => (
<RelayInfo id={k} />
{System.Sockets.map(k => (
<RelayInfo id={k.address} />
))}
</>
);

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { HexKey, NostrPrefix } from "@snort/nostr";
import { HexKey, NostrPrefix } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase";
@ -7,7 +7,7 @@ import PageSpinner from "Element/PageSpinner";
import NostrBandApi from "External/NostrBand";
import SemisolDevApi from "External/SemisolDev";
import useLogin from "Hooks/useLogin";
import { hexToBech32 } from "Util";
import { hexToBech32 } from "SnortUtils";
enum Provider {
NostrBand = 1,

View File

@ -1,10 +1,10 @@
import "./Text.css";
import { useMemo } from "react";
import { Link, useLocation } from "react-router-dom";
import { HexKey, NostrPrefix } from "@snort/nostr";
import { HexKey, NostrPrefix, validateNostrLink } from "@snort/system";
import { MentionRegex, InvoiceRegex, HashtagRegex, CashuRegex } from "Const";
import { eventLink, hexToBech32, splitByUrl, validateNostrLink } from "Util";
import { eventLink, hexToBech32, splitByUrl } from "SnortUtils";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
import Mention from "Element/Mention";

View File

@ -4,12 +4,11 @@ import "./Textarea.css";
import { useIntl } from "react-intl";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import TextareaAutosize from "react-textarea-autosize";
import { NostrPrefix } from "@snort/nostr";
import { NostrPrefix, MetadataCache } from "@snort/system";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
import { hexToBech32 } from "Util";
import { MetadataCache } from "Cache";
import { hexToBech32 } from "SnortUtils";
import { UserCache } from "Cache/UserCache";
import messages from "./messages";

View File

@ -2,10 +2,17 @@ import "./Thread.css";
import { useMemo, useState, ReactNode } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useLocation, Link, useParams } from "react-router-dom";
import { TaggedRawEvent, u256, EventKind, NostrPrefix } from "@snort/nostr";
import { EventExt, Thread as ThreadInfo } from "System/EventExt";
import {
TaggedRawEvent,
u256,
EventKind,
NostrPrefix,
EventExt,
Thread as ThreadInfo,
parseNostrLink,
} from "@snort/system";
import { eventLink, unwrap, getReactions, parseNostrLink, getAllReactions, findTag } from "Util";
import { eventLink, unwrap, getReactions, getAllReactions, findTag } from "SnortUtils";
import BackButton from "Element/BackButton";
import Note from "Element/Note";
import NoteGhost from "Element/NoteGhost";

View File

@ -2,10 +2,10 @@ import "./Timeline.css";
import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent, EventKind, u256 } from "@snort/nostr";
import { TaggedRawEvent, EventKind, u256 } from "@snort/system";
import Icon from "Icons/Icon";
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "Util";
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
import ProfileImage from "Element/ProfileImage";
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
import LoadMore from "Element/LoadMore";
@ -143,7 +143,7 @@ const Timeline = (props: TimelineProps) => {
)}
{mainFeed.map(eventElement)}
{(props.loadMore === undefined || props.loadMore === true) && (
<LoadMore onLoadMore={feed.loadMore} shouldLoadMore={!feed.loading}>
<LoadMore onLoadMore={() => feed.loadMore()} shouldLoadMore={!feed.loading}>
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />
<Skeleton width="100%" height="120px" margin="0 0 16px 0" />

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import { NostrEvent, TaggedRawEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";
@ -7,7 +7,7 @@ import Note from "Element/Note";
import NostrBandApi from "External/NostrBand";
export default function TrendingNotes() {
const [posts, setPosts] = useState<Array<RawEvent>>();
const [posts, setPosts] = useState<Array<NostrEvent>>();
async function loadTrendingNotes() {
const api = new NostrBandApi();

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase";

View File

@ -1,10 +1,10 @@
import { MouseEvent } from "react";
import { useNavigate, Link } from "react-router-dom";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "Util";
import { profileLink } from "SnortUtils";
export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) {
const user = useUserProfile(pubkey);

View File

@ -1,17 +1,17 @@
import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr";
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon";
import Spinner from "Icons/Spinner";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile } from "Util";
import { openFile } from "SnortUtils";
import Textarea from "./Textarea";
export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
const [msg, setMsg] = useState("");
const [sending, setSending] = useState(false);
const [uploading, setUploading] = useState(false);
const [otherEvents, setOtherEvents] = useState<Array<RawEvent>>([]);
const [otherEvents, setOtherEvents] = useState<Array<NostrEvent>>([]);
const [error, setError] = useState("");
const publisher = useEventPublisher();
const uploader = useFileUpload();

View File

@ -1,13 +1,13 @@
import "./Zap.css";
import { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { HexKey, TaggedRawEvent } from "@snort/nostr";
import { HexKey, TaggedRawEvent } from "@snort/system";
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "Util";
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "SnortUtils";
import { formatShort } from "Number";
import Text from "Element/Text";
import ProfileImage from "Element/ProfileImage";
import { findTag } from "Util";
import { findTag } from "SnortUtils";
import { UserCache } from "Cache/UserCache";
import useLogin from "Hooks/useLogin";

View File

@ -1,6 +1,6 @@
import "./ZapButton.css";
import { useState } from "react";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { useUserProfile } from "Hooks/useUserProfile";
import SendSats from "Element/SendSats";

View File

@ -1,12 +1,12 @@
import "./ZapstrEmbed.css";
import { Link } from "react-router-dom";
import { encodeTLV, NostrPrefix, RawEvent } from "@snort/nostr";
import { encodeTLV, NostrPrefix, NostrEvent } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import ProfileImage from "Element/ProfileImage";
import { FormattedMessage } from "react-intl";
export default function ZapstrEmbed({ ev }: { ev: RawEvent }) {
export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
const media = ev.tags.find(a => a[0] === "media");
const cover = ev.tags.find(a => a[0] === "cover");
const subject = ev.tags.find(a => a[0] === "subject");

View File

@ -1,4 +1,4 @@
import { RawEvent } from "@snort/nostr";
import { NostrEvent } from "@snort/system";
export interface TrendingUser {
pubkey: string;
@ -9,8 +9,8 @@ export interface TrendingUserResponse {
}
export interface TrendingNote {
event: RawEvent;
author: RawEvent; // kind0 event
event: NostrEvent;
author: NostrEvent; // kind0 event
}
export interface TrendingNoteResponse {

View File

@ -1,18 +1,18 @@
type HookFn = () => void;
type HookFn<TSnapshot> = (e?: TSnapshot) => void;
interface HookFilter {
fn: HookFn;
interface HookFilter<TSnapshot> {
fn: HookFn<TSnapshot>;
}
/**
* Simple React hookable store with manual change notifications
*/
export default abstract class ExternalStore<TSnapshot> {
#hooks: Array<HookFilter> = [];
#hooks: Array<HookFilter<TSnapshot>> = [];
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
#changed = true;
hook(fn: HookFn) {
hook(fn: HookFn<TSnapshot>) {
this.#hooks.push({
fn,
});
@ -32,9 +32,9 @@ export default abstract class ExternalStore<TSnapshot> {
return this.#snapshot;
}
protected notifyChange() {
protected notifyChange(sn?: TSnapshot) {
this.#changed = true;
this.#hooks.forEach(h => h.fn());
this.#hooks.forEach(h => h.fn(sn));
}
abstract takeSnapshot(): TSnapshot;

View File

@ -1,9 +1,7 @@
import { useMemo } from "react";
import { EventKind, HexKey, Lists } from "@snort/nostr";
import { EventKind, HexKey, Lists, RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system";
import { unwrap, findTag, chunks } from "Util";
import { RequestBuilder } from "System";
import { FlatNoteStore, ReplaceableNoteStore } from "System/NoteCollection";
import { unwrap, findTag, chunks } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
type BadgeAwards = {

View File

@ -1,4 +1,4 @@
import { HexKey, Lists } from "@snort/nostr";
import { HexKey, Lists } from "@snort/system";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
import useLogin from "Hooks/useLogin";

View File

@ -1,9 +1,8 @@
import { useMemo } from "react";
import { NostrPrefix } from "@snort/nostr";
import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { RequestBuilder, ReplaceableNoteStore } from "System";
import { NostrLink, unwrap } from "Util";
import { unwrap } from "SnortUtils";
export default function useEventFeed(link: NostrLink) {
const sub = useMemo(() => {

View File

@ -1,12 +1,13 @@
import { useMemo } from "react";
import useLogin from "Hooks/useLogin";
import { EventPublisher } from "System/EventPublisher";
import { EventPublisher } from "@snort/system";
import { System } from "index";
export default function useEventPublisher() {
const { publicKey, privateKey } = useLogin();
return useMemo(() => {
if (publicKey) {
return new EventPublisher(publicKey, privateKey);
return new EventPublisher(System, publicKey, privateKey);
}
}, [publicKey, privateKey]);
}

View File

@ -1,7 +1,6 @@
import { useMemo } from "react";
import { HexKey, EventKind } from "@snort/nostr";
import { HexKey, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useFollowersFeed(pubkey?: HexKey) {

View File

@ -1,7 +1,6 @@
import { useMemo } from "react";
import { HexKey, TaggedRawEvent, EventKind } from "@snort/nostr";
import { HexKey, TaggedRawEvent, EventKind, PubkeyReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,25 +1,27 @@
import { useEffect, useMemo } from "react";
import { TaggedRawEvent, Lists, EventKind } from "@snort/nostr";
import { TaggedRawEvent, Lists, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
import debug from "debug";
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "Util";
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
import { makeNotification, sendNotification } from "Notifications";
import useEventPublisher from "Feed/EventPublisher";
import { getMutedKeys } from "Feed/MuteList";
import useModeration from "Hooks/useModeration";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { DmCache } from "Cache";
import useLogin from "Hooks/useLogin";
import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login";
import { SnortPubKey } from "Const";
import { SubscriptionEvent } from "Subscription";
import useRelaysFeedFollows from "./RelaysFeedFollows";
import { UserRelays } from "Cache/UserRelayCache";
/**
* Managed loading data for the current logged in user
*/
export default function useLoginFeed() {
const login = useLogin();
const { publicKey: pubKey, readNotifications } = login;
const { publicKey: pubKey, readNotifications, follows } = login;
const { isMuted } = useModeration();
const publisher = useEventPublisher();
@ -39,6 +41,7 @@ export default function useLoginFeed() {
.limit(1);
const dmSince = DmCache.newest();
debug("LoginFeed")("Loading dms since %s", new Date(dmSince * 1000).toISOString());
b.withFilter().authors([pubKey]).kinds([EventKind.DirectMessage]).since(dmSince);
b.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pubKey]).since(dmSince);
return b;
@ -171,8 +174,12 @@ export default function useLoginFeed() {
}
}, [listsFeed]);
/*const fRelays = useRelaysFeedFollows(follows);
useEffect(() => {
FollowsRelays.bulkSet(fRelays).catch(console.error);
}, [dispatch, fRelays]);*/
UserRelays.buffer(follows.item).catch(console.error);
}, [follows.item]);
const fRelays = useRelaysFeedFollows(follows.item);
useEffect(() => {
UserRelays.bulkSet(fRelays).catch(console.error);
}, [fRelays]);
}

View File

@ -1,8 +1,14 @@
import { useMemo } from "react";
import { HexKey, TaggedRawEvent, Lists, EventKind } from "@snort/nostr";
import {
HexKey,
TaggedRawEvent,
Lists,
EventKind,
ParameterizedReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import { getNewest } from "Util";
import { ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
import { getNewest } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,4 +1,4 @@
import { HexKey, Lists } from "@snort/nostr";
import { HexKey, Lists } from "@snort/system";
import useNotelistSubscription from "Hooks/useNotelistSubscription";
import useLogin from "Hooks/useLogin";

View File

@ -1,18 +1,6 @@
import { useSyncExternalStore } from "react";
import { StateSnapshot } from "@snort/nostr";
import { System } from "System";
const noop = () => {
return () => undefined;
};
const noopState = (): StateSnapshot | undefined => {
return undefined;
};
import { System } from "index";
export default function useRelayState(addr: string) {
const c = System.Sockets.get(addr);
return useSyncExternalStore<StateSnapshot | undefined>(
c?.StatusHook.bind(c) ?? noop,
c?.GetState.bind(c) ?? noopState
);
const c = System.Sockets.find(a => a.address === addr);
return c;
}

View File

@ -1,8 +1,6 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, EventKind } from "@snort/nostr";
import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system";
import { RequestBuilder } from "System";
import { ReplaceableNoteStore } from "System/NoteCollection";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useRelaysFeed(pubkey?: HexKey) {

View File

@ -1,73 +1,88 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, TaggedRawEvent, RelaySettings, EventKind } from "@snort/nostr";
import {
HexKey,
FullRelaySettings,
TaggedRawEvent,
RelaySettings,
EventKind,
PubkeyReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import debug from "debug";
import { sanitizeRelayUrl } from "Util";
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
import { sanitizeRelayUrl } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
import { UserRelays } from "Cache/UserRelayCache";
type UserRelayMap = Record<HexKey, Array<FullRelaySettings>>;
interface RelayList {
pubkey: string;
created_at: number;
relays: FullRelaySettings[];
}
export default function useRelaysFeedFollows(pubkeys: HexKey[]): UserRelayMap {
export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array<RelayList> {
const sub = useMemo(() => {
const b = new RequestBuilder(`relays:follows`);
b.withFilter().authors(pubkeys).kinds([EventKind.Relays, EventKind.ContactList]);
const since = UserRelays.newest();
debug("LoginFeed")("Loading relay lists since %s", new Date(since * 1000).toISOString());
b.withFilter().authors(pubkeys).kinds([EventKind.Relays, EventKind.ContactList]).since(since);
return b;
}, [pubkeys]);
function mapFromRelays(notes: Array<TaggedRawEvent>): UserRelayMap {
return Object.fromEntries(
notes.map(ev => {
return [
ev.pubkey,
ev.tags
.map(a => {
return {
url: sanitizeRelayUrl(a[1]),
settings: {
read: a[2] === "read" || a[2] === undefined,
write: a[2] === "write" || a[2] === undefined,
},
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
];
})
);
function mapFromRelays(notes: Array<TaggedRawEvent>): Array<RelayList> {
return notes.map(ev => {
return {
pubkey: ev.pubkey,
created_at: ev.created_at,
relays: ev.tags
.map(a => {
return {
url: sanitizeRelayUrl(a[1]),
settings: {
read: a[2] === "read" || a[2] === undefined,
write: a[2] === "write" || a[2] === undefined,
},
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
};
});
}
function mapFromContactList(notes: Array<TaggedRawEvent>): UserRelayMap {
return Object.fromEntries(
notes.map(ev => {
if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) {
try {
const relays: Record<string, RelaySettings> = JSON.parse(ev.content);
return [
ev.pubkey,
Object.entries(relays)
.map(([k, v]) => {
return {
url: sanitizeRelayUrl(k),
settings: v,
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
];
} catch {
// ignored
}
// instead of discarding the follow list we should also use it for follow graph
function mapFromContactList(notes: Array<TaggedRawEvent>): Array<RelayList> {
return notes.map(ev => {
if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) {
try {
const relays: Record<string, RelaySettings> = JSON.parse(ev.content);
return {
pubkey: ev.pubkey,
created_at: ev.created_at,
relays: Object.entries(relays)
.map(([k, v]) => {
return {
url: sanitizeRelayUrl(k),
settings: v,
} as FullRelaySettings;
})
.filter(a => a.url !== undefined),
};
} catch {
// ignored
}
return [ev.pubkey, []];
})
);
}
return {
pubkey: ev.pubkey,
created_at: 0,
relays: [],
};
});
}
const relays = useRequestBuilder<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? [];
const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? [];
return useMemo(() => {
return {
...mapFromContactList(notesContactLists),
...mapFromRelays(notesRelays),
} as UserRelayMap;
return [...mapFromContactList(notesContactLists), ...mapFromRelays(notesRelays)];
}, [relays]);
}

View File

@ -1,15 +1,22 @@
import { useEffect, useMemo, useState } from "react";
import { u256, EventKind } from "@snort/nostr";
import { u256, EventKind, NostrLink, FlatNoteStore, RequestBuilder } from "@snort/system";
import { appendDedupe, NostrLink } from "Util";
import { FlatNoteStore, RequestBuilder } from "System";
import { appendDedupe } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";
interface RelayTaggedEventId {
id: u256;
relay?: string;
}
export default function useThreadFeed(link: NostrLink) {
const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]);
const linkTagged = {
id: link.id,
relay: link.relays?.[0],
};
const [trackingEvents, setTrackingEvent] = useState<Array<RelayTaggedEventId>>([linkTagged]);
const [trackingATags, setTrackingATags] = useState<string[]>([]);
const [allEvents, setAllEvents] = useState<u256[]>([link.id]);
const [allEvents, setAllEvents] = useState<Array<RelayTaggedEventId>>([linkTagged]);
const pref = useLogin().preferences;
const sub = useMemo(() => {
@ -17,7 +24,10 @@ export default function useThreadFeed(link: NostrLink) {
sub.withOptions({
leaveOpen: true,
});
sub.withFilter().ids(trackingEvents);
const fTracking = sub.withFilter();
for (const te of trackingEvents) {
fTracking.id(te.id, te.relay);
}
sub
.withFilter()
.kinds(
@ -25,7 +35,10 @@ export default function useThreadFeed(link: NostrLink) {
? [EventKind.Reaction, EventKind.TextNote, EventKind.Repost, EventKind.ZapReceipt]
: [EventKind.TextNote, EventKind.ZapReceipt, EventKind.Repost]
)
.tag("e", allEvents);
.tag(
"e",
allEvents.map(a => a.id)
);
if (trackingATags.length > 0) {
const parsed = trackingATags.map(a => a.split(":"));
@ -45,16 +58,27 @@ export default function useThreadFeed(link: NostrLink) {
useEffect(() => {
setTrackingATags([]);
setTrackingEvent([link.id]);
setAllEvents([link.id]);
setTrackingEvent([linkTagged]);
setAllEvents([linkTagged]);
}, [link.id]);
useEffect(() => {
if (store.data) {
const mainNotes = store.data?.filter(a => a.kind === EventKind.TextNote || a.kind === EventKind.Polls) ?? [];
const eTags = mainNotes.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a));
const eTags = mainNotes
.map(a =>
a.tags
.filter(b => b[0] === "e")
.map(b => {
return {
id: b[1],
relay: b[2],
};
})
)
.flat();
const eTagsMissing = eTags.filter(a => !mainNotes.some(b => b.id === a.id));
setTrackingEvent(s => appendDedupe(s, eTagsMissing));
setAllEvents(s => appendDedupe(s, eTags));

View File

@ -1,8 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { EventKind, u256 } from "@snort/nostr";
import { EventKind, u256, FlatNoteStore, RequestBuilder } from "@snort/system";
import { unixNow, unwrap, tagFilterOfTextRepost } from "Util";
import { FlatNoteStore, RequestBuilder } from "System";
import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useTimelineWindow from "Hooks/useTimelineWindow";
import useLogin from "Hooks/useLogin";
@ -134,7 +133,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
}, [options.relay]);
const subNext = useMemo(() => {
const rb = new RequestBuilder(`timeline-related:${subject.type}`);
const rb = new RequestBuilder(`timeline-related:${subject.type}:${subject.discriminator}`);
if (trackingEvents.length > 0) {
rb.withFilter()
.kinds(

View File

@ -1,8 +1,7 @@
import { useMemo } from "react";
import { HexKey, EventKind } from "@snort/nostr";
import { HexKey, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
import { parseZap } from "Element/Zap";
import { FlatNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
export default function useZapsFeed(pubkey?: HexKey) {

View File

@ -1,6 +1,6 @@
import * as utils from "@noble/curves/abstract/utils";
import * as base64 from "@protobufjs/base64";
import { hmacSha256, unwrap } from "Util";
import { hmacSha256, unwrap } from "SnortUtils";
import useLogin from "Hooks/useLogin";
export interface ImgProxySettings {

View File

@ -1,9 +1,9 @@
import { useSyncExternalStore } from "react";
import { HexKey, u256 } from "@snort/nostr";
import { HexKey, u256 } from "@snort/system";
import { InteractionCache } from "Cache/EventInteractionCache";
import { EventInteraction } from "Db";
import { sha256, unwrap } from "Util";
import { sha256, unwrap } from "SnortUtils";
export function useInteractionCache(pubkey?: HexKey, event?: u256) {
const id = event && pubkey ? sha256(event + pubkey) : undefined;

View File

@ -4,7 +4,7 @@ import { EmailRegex, MnemonicRegex } from "Const";
import { LoginStore } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { getNip05PubKey } from "Pages/LoginPage";
import { bech32ToHex } from "Util";
import { bech32ToHex } from "SnortUtils";
export default function useLoginHandler() {
const { formatMessage } = useIntl();

View File

@ -1,8 +1,8 @@
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import useLogin from "Hooks/useLogin";
import { setBlocked, setMuted } from "Login";
import { appendDedupe } from "Util";
import { appendDedupe } from "SnortUtils";
export default function useModeration() {
const login = useLogin();

View File

@ -1,7 +1,13 @@
import { useMemo } from "react";
import { HexKey, Lists, EventKind } from "@snort/nostr";
import {
HexKey,
Lists,
EventKind,
FlatNoteStore,
ParameterizedReplaceableNoteStore,
RequestBuilder,
} from "@snort/system";
import { FlatNoteStore, ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
import useRequestBuilder from "Hooks/useRequestBuilder";
import useLogin from "Hooks/useLogin";

View File

@ -1,65 +0,0 @@
import { HexKey } from "@snort/nostr";
import { useMemo } from "react";
import { FollowsRelays } from "State/Relays";
import { unwrap } from "Util";
export type RelayPicker = ReturnType<typeof useRelaysForFollows>;
/**
* Number of relays to pick per pubkey
*/
const PickNRelays = 2;
export default function useRelaysForFollows(keys: Array<HexKey>) {
return useMemo(() => {
if (keys.length === 0) {
return {};
}
const allRelays = keys.map(a => {
return {
key: a,
relays: FollowsRelays.snapshot.get(a),
};
});
const missing = allRelays.filter(a => a.relays === undefined);
const hasRelays = allRelays.filter(a => a.relays !== undefined);
const relayUserMap = hasRelays.reduce((acc, v) => {
for (const r of unwrap(v.relays)) {
if (!acc.has(r.url)) {
acc.set(r.url, new Set([v.key]));
} else {
unwrap(acc.get(r.url)).add(v.key);
}
}
return acc;
}, new Map<string, Set<HexKey>>());
const topRelays = [...relayUserMap.entries()].sort(([, v], [, v1]) => v1.size - v.size);
// <relay, key[]> - count keys per relay
// <key, relay[]> - pick n top relays
// <relay, key[]> - map keys per relay (for subscription filter)
const userPickedRelays = keys.map(k => {
// pick top 3 relays for this key
const relaysForKey = topRelays
.filter(([, v]) => v.has(k))
.slice(0, PickNRelays)
.map(([k]) => k);
return { k, relaysForKey };
});
const pickedRelays = new Set(userPickedRelays.map(a => a.relaysForKey).flat());
const picked = Object.fromEntries(
[...pickedRelays].map(a => {
const keysOnPickedRelay = new Set(userPickedRelays.filter(b => b.relaysForKey.includes(a)).map(b => b.k));
return [a, [...keysOnPickedRelay]];
})
);
picked[""] = missing.map(a => a.key);
console.debug(picked);
return picked;
}, [keys]);
}

View File

@ -1,39 +1,30 @@
import { useSyncExternalStore } from "react";
import { RequestBuilder, System } from "System";
import { EmptySnapshot, NoteStore, StoreSnapshot } from "System/NoteCollection";
import { unwrap } from "Util";
import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system";
import { unwrap } from "SnortUtils";
import { System } from "index";
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
type: { new (): TStore },
rb: RequestBuilder | null,
debounced?: number
rb: RequestBuilder | null
) => {
const subscribe = (onChanged: () => void) => {
const store = System.Query<TStore>(type, rb);
let t: ReturnType<typeof setTimeout> | undefined;
const release = store.hook(() => {
if (!t) {
t = setTimeout(() => {
clearTimeout(t);
t = undefined;
onChanged();
}, debounced ?? 500);
}
});
if (rb) {
const q = System.Query<TStore>(type, rb);
const release = q.feed.hook(onChanged);
q.uncancel();
return () => {
q.cancel();
release();
};
}
return () => {
if (rb?.id) {
System.CancelQuery(rb.id);
}
release();
// noop
};
};
const getState = (): StoreSnapshot<TSnapshot> => {
if (rb?.id) {
const q = System.GetQuery(rb.id);
if (q) {
return unwrap(q).feed?.snapshot as StoreSnapshot<TSnapshot>;
}
const q = System.GetQuery(rb?.id ?? "");
if (q) {
return unwrap(q).feed?.snapshot as StoreSnapshot<TSnapshot>;
}
return EmptySnapshot as StoreSnapshot<TSnapshot>;
};

View File

@ -1,9 +1,10 @@
import { useSyncExternalStore } from "react";
import { System, SystemSnapshot } from "System";
import { SystemSnapshot } from "@snort/system";
import { System } from "index";
export default function useSystemState() {
return useSyncExternalStore<SystemSnapshot>(
cb => System.hook(cb),
() => System.getSnapshot()
() => System.snapshot()
);
}

View File

@ -1,9 +1,8 @@
import { useEffect, useSyncExternalStore } from "react";
import { HexKey } from "@snort/nostr";
import { MetadataCache } from "Cache";
import { HexKey, MetadataCache } from "@snort/system";
import { UserCache } from "Cache/UserCache";
import { ProfileLoader } from "System/ProfileCache";
import { ProfileLoader } from "index";
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
const user = useSyncExternalStore<MetadataCache | undefined>(

View File

@ -1,6 +1,6 @@
import { HexKey, RawEvent } from "@snort/nostr";
import { HexKey, NostrEvent } from "@snort/system";
import { EmailRegex } from "Const";
import { bech32ToText, unwrap } from "Util";
import { bech32ToText, unwrap } from "SnortUtils";
const PayServiceTag = "payRequest";
@ -119,7 +119,7 @@ export class LNURL {
* @param zap
* @returns
*/
async getInvoice(amount: number, comment?: string, zap?: RawEvent) {
async getInvoice(amount: number, comment?: string, zap?: NostrEvent) {
const callback = new URL(unwrap(this.#service?.callback));
const query = new Map<string, string>();

View File

@ -1,13 +1,13 @@
import { HexKey, RelaySettings } from "@snort/nostr";
import { HexKey, RelaySettings, EventPublisher } from "@snort/system";
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { DefaultRelays, SnortPubKey } from "Const";
import { LoginStore, UserPreferences, LoginSession } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "Util";
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "SnortUtils";
import { SubscriptionEvent } from "Subscription";
import { EventPublisher } from "System/EventPublisher";
import { System } from "index";
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
if (state.relays.timestamp >= createdAt) {
@ -78,7 +78,7 @@ export async function generateNewLogin() {
}
const publicKey = utils.bytesToHex(secp.schnorr.getPublicKey(privateKey));
const publisher = new EventPublisher(publicKey, privateKey);
const publisher = new EventPublisher(System, publicKey, privateKey);
const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays);
publisher.broadcast(ev);

View File

@ -1,4 +1,4 @@
import { HexKey, RelaySettings, u256 } from "@snort/nostr";
import { HexKey, RelaySettings, u256 } from "@snort/system";
import { UserPreferences } from "Login";
import { SubscriptionEvent } from "Subscription";

View File

@ -1,12 +1,12 @@
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { HexKey, RelaySettings } from "@snort/nostr";
import { HexKey, RelaySettings } from "@snort/system";
import { DefaultRelays } from "Const";
import ExternalStore from "ExternalStore";
import { LoginSession } from "Login";
import { deepClone, sanitizeRelayUrl, unwrap } from "Util";
import { deepClone, sanitizeRelayUrl, unwrap } from "SnortUtils";
import { DefaultPreferences, UserPreferences } from "./Preferences";
const AccountStoreKey = "sessions";

View File

@ -1,5 +1,4 @@
import { EventKind } from "@snort/nostr";
import { EventPublisher } from "System/EventPublisher";
import { EventKind, EventPublisher } from "@snort/system";
import { ServiceError, ServiceProvider } from "./ServiceProvider";
export interface ManageHandle {

View File

@ -1,5 +1,5 @@
import DnsOverHttpResolver from "dns-over-http-resolver";
import { bech32ToHex } from "Util";
import { bech32ToHex } from "SnortUtils";
const resolver = new DnsOverHttpResolver();
interface NostrJson {

View File

@ -1,11 +1,9 @@
import Nostrich from "nostrich.webp";
import { TaggedRawEvent } from "@snort/nostr";
import { EventKind } from "@snort/nostr";
import { MetadataCache } from "Cache";
import { TaggedRawEvent, EventKind, MetadataCache } from "@snort/system";
import { getDisplayName } from "Element/ProfileImage";
import { MentionRegex } from "Const";
import { tagFilterOfTextRepost, unwrap } from "Util";
import { tagFilterOfTextRepost, unwrap } from "SnortUtils";
import { UserCache } from "Cache/UserCache";
import { LoginSession } from "Login";

View File

@ -1,6 +1,6 @@
import DmWindow from "Element/DmWindow";
import { useParams } from "react-router-dom";
import { bech32ToHex } from "Util";
import { bech32ToHex } from "SnortUtils";
import "./ChatPage.css";

View File

@ -0,0 +1,9 @@
import SubDebug from "Element/SubDebug";
export default function DebugPage() {
return (
<>
<SubDebug />
</>
);
}

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { ApiHost, KieranPubKey, SnortPubKey } from "Const";
import ProfilePreview from "Element/ProfilePreview";
import ZapButton from "Element/ZapButton";
import { bech32ToHex } from "Util";
import { bech32ToHex } from "SnortUtils";
import SnortApi, { RevenueSplit, RevenueToday } from "SnortApi";
const Developers = [

View File

@ -9,21 +9,18 @@ import messages from "./messages";
import Icon from "Icons/Icon";
import { RootState } from "State/Store";
import { setShow, reset } from "State/NoteCreator";
import { System } from "System";
import { System } from "index";
import useLoginFeed from "Feed/LoginFeed";
import { totalUnread } from "Pages/MessagesPage";
import useModeration from "Hooks/useModeration";
import { NoteCreator } from "Element/NoteCreator";
import { db } from "Db";
import useEventPublisher from "Feed/EventPublisher";
import SubDebug from "Element/SubDebug";
import { preload } from "Cache";
import { useDmCache } from "Hooks/useDmsCache";
import { mapPlanName } from "./subscribe";
import useLogin from "Hooks/useLogin";
import Avatar from "Element/Avatar";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "Util";
import { profileLink } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import Toaster from "Toaster";
@ -78,9 +75,9 @@ export default function Layout() {
for (const [k, v] of Object.entries(relays.item)) {
await System.ConnectToRelay(k, v);
}
for (const [k, c] of System.Sockets) {
if (!relays.item[k] && !c.Ephemeral) {
System.DisconnectRelay(k);
for (const v of System.Sockets) {
if (!relays.item[v.address] && !v.ephemeral) {
System.DisconnectRelay(v.address);
}
}
})();
@ -112,29 +109,6 @@ export default function Layout() {
};
}, [preferences.theme]);
useEffect(() => {
// check DB support then init
db.isAvailable().then(async a => {
db.ready = a;
if (a) {
await preload();
}
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
try {
if ("registerProtocolHandler" in window.navigator) {
window.navigator.registerProtocolHandler(
"web+nostr",
`${window.location.protocol}//${window.location.host}/%s`
);
console.info("Registered protocol handler for 'web+nostr'");
}
} catch (e) {
console.error("Failed to register protocol handler", e);
}
});
}, []);
return (
<div className={pageClass}>
{!shouldHideHeader && (
@ -170,7 +144,6 @@ export default function Layout() {
<NoteCreator />
</>
)}
{window.localStorage.getItem("debug") && <SubDebug />}
<Toaster />
</div>
);

View File

@ -3,9 +3,9 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { HexKey } from "@snort/system";
import { bech32ToHex, unwrap } from "Util";
import { bech32ToHex, unwrap } from "SnortUtils";
import ZapButton from "Element/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";

View File

@ -1,11 +1,11 @@
import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { HexKey, RawEvent, NostrPrefix } from "@snort/nostr";
import { HexKey, NostrEvent, NostrPrefix } from "@snort/system";
import UnreadCount from "Element/UnreadCount";
import ProfileImage, { getDisplayName } from "Element/ProfileImage";
import { dedupe, hexToBech32, unwrap } from "Util";
import { dedupe, hexToBech32, unwrap } from "SnortUtils";
import NoteToSelf from "Element/NoteToSelf";
import useModeration from "Hooks/useModeration";
import { useDmCache } from "Hooks/useDmsCache";
@ -162,30 +162,30 @@ export function setLastReadDm(pk: HexKey) {
window.localStorage.setItem(k, now.toString());
}
export function dmTo(e: RawEvent) {
export function dmTo(e: NostrEvent) {
const firstP = e.tags.find(b => b[0] === "p");
return unwrap(firstP?.[1]);
}
export function isToSelf(e: Readonly<RawEvent>, pk: HexKey) {
export function isToSelf(e: Readonly<NostrEvent>, pk: HexKey) {
return e.pubkey === pk && dmTo(e) === pk;
}
export function dmsInChat(dms: readonly RawEvent[], pk: HexKey) {
export function dmsInChat(dms: readonly NostrEvent[], pk: HexKey) {
return dms.filter(a => a.pubkey === pk || dmTo(a) === pk);
}
export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
export function totalUnread(dms: NostrEvent[], myPubKey: HexKey) {
return extractChats(dms, myPubKey).reduce((acc, v) => (acc += v.unreadMessages), 0);
}
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
function unreadDms(dms: NostrEvent[], myPubKey: HexKey, pk: HexKey) {
if (pk === myPubKey) return 0;
const lastRead = lastReadDm(pk);
return dmsInChat(dms, pk).filter(a => a.created_at >= lastRead && a.pubkey !== myPubKey).length;
}
function newestMessage(dms: readonly RawEvent[], myPubKey: HexKey, pk: HexKey) {
function newestMessage(dms: readonly NostrEvent[], myPubKey: HexKey, pk: HexKey) {
if (pk === myPubKey) {
return dmsInChat(
dms.filter(d => isToSelf(d, myPubKey)),
@ -196,11 +196,11 @@ function newestMessage(dms: readonly RawEvent[], myPubKey: HexKey, pk: HexKey) {
return dmsInChat(dms, pk).reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
}
export function dmsForLogin(dms: readonly RawEvent[], myPubKey: HexKey) {
export function dmsForLogin(dms: readonly NostrEvent[], myPubKey: HexKey) {
return dms.filter(a => a.pubkey === myPubKey || (a.pubkey !== myPubKey && dmTo(a) === myPubKey));
}
export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
export function extractChats(dms: NostrEvent[], myPubKey: HexKey) {
const myDms = dmsForLogin(dms, myPubKey);
const keys = myDms.map(a => [a.pubkey, dmTo(a)]).flat();
const filteredKeys = dedupe(keys);

View File

@ -1,12 +1,11 @@
import { NostrPrefix } from "@snort/nostr";
import { NostrPrefix, parseNostrLink } from "@snort/system";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import Spinner from "Icons/Spinner";
import { parseNostrLink, profileLink } from "Util";
import { profileLink } from "SnortUtils";
import { getNip05PubKey } from "Pages/LoginPage";
import { System } from "System";
export default function NostrLinkHandler() {
const params = useParams();
@ -18,9 +17,6 @@ export default function NostrLinkHandler() {
async function handleLink(link: string) {
const nav = parseNostrLink(link);
if (nav) {
if ((nav.relays?.length ?? 0) > 0) {
nav.relays?.map(a => System.ConnectEphemeralRelay(a));
}
if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
navigate(`/e/${nav.encode()}`);
} else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) {

Some files were not shown because too many files have changed in this diff Show More