feat: in-memory fallback for storing user profiles #110
@ -1,4 +1,4 @@
|
|||||||
## Snort
|
## Snort
|
||||||
|
|
||||||
Snort is a nostr UI built with React, Snort intends to be fast and effecient
|
Snort is a nostr UI built with React, Snort intends to be fast and effecient
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ Snort supports the following NIP's
|
|||||||
- [x] NIP-08: Handling Mentions
|
- [x] NIP-08: Handling Mentions
|
||||||
- [x] NIP-09: Event Deletion
|
- [x] NIP-09: Event Deletion
|
||||||
- [x] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
- [x] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
|
||||||
- [ ] NIP-11: Relay Information Document
|
- [x] NIP-11: Relay Information Document
|
||||||
- [x] NIP-12: Generic Tag Queries
|
- [x] NIP-12: Generic Tag Queries
|
||||||
- [ ] NIP-13: Proof of Work
|
- [ ] NIP-13: Proof of Work
|
||||||
- [ ] NIP-14: Subject tag in text events
|
- [ ] NIP-14: Subject tag in text events
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { RelaySettings } from "Nostr/Connection";
|
import { RelaySettings } from "Nostr/Connection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add-on api for snort features
|
||||||
|
*/
|
||||||
|
export const ApiHost = "https://api.snort.social";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Websocket re-connect timeout
|
* Websocket re-connect timeout
|
||||||
*/
|
*/
|
||||||
@ -93,5 +98,4 @@ export const TidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i;
|
|||||||
/**
|
/**
|
||||||
* SoundCloud regex
|
* SoundCloud regex
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/
|
export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/
|
||||||
|
@ -33,7 +33,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
|
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
|
||||||
const user = useUserProfile(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const svc = new ServiceProvider(props.service);
|
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||||
const [error, setError] = useState<ServiceError>();
|
const [error, setError] = useState<ServiceError>();
|
||||||
const [handle, setHandle] = useState<string>("");
|
const [handle, setHandle] = useState<string>("");
|
||||||
@ -43,7 +43,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
const [showInvoice, setShowInvoice] = useState<boolean>(false);
|
const [showInvoice, setShowInvoice] = useState<boolean>(false);
|
||||||
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
|
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
|
||||||
|
|
||||||
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain]);
|
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
svc.GetConfig()
|
svc.GetConfig()
|
||||||
@ -58,7 +58,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}, [props]);
|
}, [props, svc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@ -89,7 +89,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [handle, domain]);
|
}, [handle, domain, domainConfig, svc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (registerResponse && showInvoice) {
|
if (registerResponse && showInvoice) {
|
||||||
@ -111,7 +111,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}, 2_000);
|
}, 2_000);
|
||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
}
|
}
|
||||||
}, [registerResponse, showInvoice])
|
}, [registerResponse, showInvoice, svc])
|
||||||
|
|
||||||
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||||
let whyMap = new Map([
|
let whyMap = new Map([
|
||||||
@ -159,7 +159,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
<p>Find out more info about {props.name} at <a href={props.link} target="_blank" rel="noreferrer">{props.link}</a></p>
|
<p>Find out more info about {props.name} at <a href={props.link} target="_blank" rel="noreferrer">{props.link}</a></p>
|
||||||
{error && <b className="error">{error.error}</b>}
|
{error && <b className="error">{error.error}</b>}
|
||||||
{!registerStatus && <div className="flex mb10">
|
{!registerStatus && <div className="flex mb10">
|
||||||
<input type="text" placeholder="Handle" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
<input type="text" placeholder="Handle" value={handle} onChange={(e) => setHandle(e.target.value.toLowerCase())} />
|
||||||
@
|
@
|
||||||
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
||||||
{serviceConfig?.domains.map(a => <option key={a.name}>{a.name}</option>)}
|
{serviceConfig?.domains.map(a => <option key={a.name}>{a.name}</option>)}
|
||||||
|
@ -16,13 +16,8 @@
|
|||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
background-color: var(--note-bg);
|
background-color: var(--note-bg);
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
max-width: -webkit-fill-available;
|
max-width: stretch;
|
||||||
max-width: -moz-available;
|
min-width: stretch;
|
||||||
max-width: fill-available;
|
|
||||||
min-width: 100%;
|
|
||||||
min-width: -webkit-fill-available;
|
|
||||||
min-width: -moz-available;
|
|
||||||
min-width: fill-available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-creator .actions {
|
.note-creator .actions {
|
||||||
|
@ -31,8 +31,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const [reply, setReply] = useState(false);
|
const [reply, setReply] = useState(false);
|
||||||
const [tip, setTip] = useState(false);
|
const [tip, setTip] = useState(false);
|
||||||
const isMine = ev.RootPubKey === login;
|
const isMine = ev.RootPubKey === login;
|
||||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related]);
|
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
|
||||||
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related]);
|
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related, ev]);
|
||||||
const groupReactions = useMemo(() => {
|
const groupReactions = useMemo(() => {
|
||||||
return reactions?.reduce((acc, { content }) => {
|
return reactions?.reduce((acc, { content }) => {
|
||||||
let r = normalizeReaction(content);
|
let r = normalizeReaction(content);
|
||||||
|
@ -16,7 +16,8 @@ export interface NoteReactionProps {
|
|||||||
root?: TaggedRawEvent
|
root?: TaggedRawEvent
|
||||||
}
|
}
|
||||||
export default function NoteReaction(props: NoteReactionProps) {
|
export default function NoteReaction(props: NoteReactionProps) {
|
||||||
const ev = useMemo(() => props["data-ev"] || new NEvent(props.data), [props.data, props["data-ev"]])
|
const { ["data-ev"]: dataEv, data } = props;
|
||||||
|
const ev = useMemo(() => dataEv || new NEvent(data), [data, dataEv])
|
||||||
|
|
||||||
const refEvent = useMemo(() => {
|
const refEvent = useMemo(() => {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
@ -32,19 +33,6 @@ export default function NoteReaction(props: NoteReactionProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapReaction(c: string) {
|
|
||||||
switch (c) {
|
|
||||||
case "+": return "❤️";
|
|
||||||
case "-": return "👎";
|
|
||||||
default: {
|
|
||||||
if (c.length === 0) {
|
|
||||||
return "❤️";
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some clients embed the reposted note in the content
|
* Some clients embed the reposted note in the content
|
||||||
*/
|
*/
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import "./Relay.css"
|
import "./Relay.css"
|
||||||
|
|
||||||
import { faPlug, faTrash, faSquareCheck, faSquareXmark, faWifi, faUpload, faDownload, faPlugCircleXmark, faEllipsisVertical } from "@fortawesome/free-solid-svg-icons";
|
import { faPlug, faSquareCheck, faSquareXmark, faWifi, faPlugCircleXmark, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||||
import useRelayState from "Feed/RelayState";
|
import useRelayState from "Feed/RelayState";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { removeRelay, setRelays } from "State/Login";
|
import { setRelays } from "State/Login";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { RelaySettings } from "Nostr/Connection";
|
import { RelaySettings } from "Nostr/Connection";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export interface RelayProps {
|
export interface RelayProps {
|
||||||
addr: string
|
addr: string
|
||||||
@ -15,11 +16,11 @@ export interface RelayProps {
|
|||||||
|
|
||||||
export default function Relay(props: RelayProps) {
|
export default function Relay(props: RelayProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||||
const relaySettings = allRelaySettings[props.addr];
|
const relaySettings = allRelaySettings[props.addr];
|
||||||
const state = useRelayState(props.addr);
|
const state = useRelayState(props.addr);
|
||||||
const name = useMemo(() => new URL(props.addr).host, [props.addr]);
|
const name = useMemo(() => new URL(props.addr).host, [props.addr]);
|
||||||
const [showExtra, setShowExtra] = useState(false);
|
|
||||||
|
|
||||||
function configure(o: RelaySettings) {
|
function configure(o: RelaySettings) {
|
||||||
dispatch(setRelays({
|
dispatch(setRelays({
|
||||||
@ -62,28 +63,13 @@ export default function Relay(props: RelayProps) {
|
|||||||
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="icon-btn" onClick={() => setShowExtra(s => !s)}>
|
<span className="icon-btn" onClick={() => navigate(name)}>
|
||||||
<FontAwesomeIcon icon={faEllipsisVertical} />
|
<FontAwesomeIcon icon={faGear} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showExtra ? <div className="flex relay-extra w-max">
|
|
||||||
<div className="f-1">
|
|
||||||
<FontAwesomeIcon icon={faUpload} /> {state?.events.send}
|
|
||||||
</div>
|
|
||||||
<div className="f-1">
|
|
||||||
<FontAwesomeIcon icon={faDownload} /> {state?.events.received}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="f-1">
|
|
||||||
Delete
|
|
||||||
<span className="icon-btn" onClick={() => dispatch(removeRelay(props.addr))}>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import useTimelineFeed, { TimelineSubject } from "Feed/TimelineFeed";
|
import useTimelineFeed, { TimelineSubject } from "Feed/TimelineFeed";
|
||||||
import { TaggedRawEvent } from "Nostr";
|
import { TaggedRawEvent } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
@ -23,17 +23,17 @@ export default function Timeline({ subject, postsOnly = false, method }: Timelin
|
|||||||
method
|
method
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterPosts = (notes: TaggedRawEvent[]) => {
|
const filterPosts = useCallback((nts: TaggedRawEvent[]) => {
|
||||||
return [...notes].sort((a, b) => b.created_at - a.created_at)?.filter(a => postsOnly ? !a.tags.some(b => b[0] === "e") : true);
|
return [...nts].sort((a, b) => b.created_at - a.created_at)?.filter(a => postsOnly ? !a.tags.some(b => b[0] === "e") : true);
|
||||||
}
|
}, [postsOnly]);
|
||||||
|
|
||||||
const mainFeed = useMemo(() => {
|
const mainFeed = useMemo(() => {
|
||||||
return filterPosts(main.notes);
|
return filterPosts(main.notes);
|
||||||
}, [main]);
|
}, [main, filterPosts]);
|
||||||
|
|
||||||
const latestFeed = useMemo(() => {
|
const latestFeed = useMemo(() => {
|
||||||
return filterPosts(latest.notes).filter(a => !mainFeed.some(b => b.id === a.id));
|
return filterPosts(latest.notes).filter(a => !mainFeed.some(b => b.id === a.id));
|
||||||
}, [latest]);
|
}, [latest, mainFeed, filterPosts]);
|
||||||
|
|
||||||
function eventElement(e: TaggedRawEvent) {
|
function eventElement(e: TaggedRawEvent) {
|
||||||
switch (e.kind) {
|
switch (e.kind) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useLiveQuery } from "dexie-react-hooks";
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "State/Users";
|
||||||
|
@ -38,7 +38,8 @@ function notesReducer(state: NoteStore, arg: ReducerArg) {
|
|||||||
if (!Array.isArray(evs)) {
|
if (!Array.isArray(evs)) {
|
||||||
evs = [evs];
|
evs = [evs];
|
||||||
}
|
}
|
||||||
evs = evs.filter(a => !state.notes.some(b => b.id === a.id));
|
let existingIds = new Set(state.notes.map(a => a.id));
|
||||||
|
evs = evs.filter(a => !existingIds.has(a.id));
|
||||||
if (evs.length === 0) {
|
if (evs.length === 0) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -83,7 +84,7 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
|
|||||||
setSubDebounced(sub);
|
setSubDebounced(sub);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [sub]);
|
}, [sub, options]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sub) {
|
if (sub) {
|
||||||
@ -115,6 +116,7 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [subDebounce]);
|
}, [subDebounce]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return debounce(DebounceMs, () => {
|
return debounce(DebounceMs, () => {
|
||||||
setDebounceOutput(s => s += 1);
|
setDebounceOutput(s => s += 1);
|
||||||
|
@ -36,7 +36,7 @@ export default function useThreadFeed(id: u256) {
|
|||||||
thisSub.AddSubscription(subRelated);
|
thisSub.AddSubscription(subRelated);
|
||||||
|
|
||||||
return thisSub;
|
return thisSub;
|
||||||
}, [trackingEvents]);
|
}, [trackingEvents, pref, id]);
|
||||||
|
|
||||||
const main = useSubscription(sub, { leaveOpen: true });
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { u256 } from "Nostr";
|
import { u256 } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import { Subscriptions } from "Nostr/Subscriptions";
|
import { Subscriptions } from "Nostr/Subscriptions";
|
||||||
@ -19,15 +19,15 @@ export interface TimelineSubject {
|
|||||||
|
|
||||||
export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
|
export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
|
||||||
const now = unixNow();
|
const now = unixNow();
|
||||||
const [window, setWindow] = useState<number>(60 * 60);
|
const [window] = useState<number>(60 * 60);
|
||||||
const [until, setUntil] = useState<number>(now);
|
const [until, setUntil] = useState<number>(now);
|
||||||
const [since, setSince] = useState<number>(now - window);
|
const [since, setSince] = useState<number>(now - window);
|
||||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||||
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
|
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
|
||||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||||
|
|
||||||
function createSub() {
|
const createSub = useCallback(() => {
|
||||||
if (subject.type !== "global" && subject.items.length == 0) {
|
if (subject.type !== "global" && subject.items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}, [subject.type, subject.items]);
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
let sub = createSub();
|
let sub = createSub();
|
||||||
@ -78,7 +78,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sub;
|
return sub;
|
||||||
}, [subject.type, subject.items, until, since, window]);
|
}, [until, since, options.method, pref, createSub]);
|
||||||
|
|
||||||
const main = useSubscription(sub, { leaveOpen: true });
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
subLatest.Since = Math.floor(new Date().getTime() / 1000);
|
subLatest.Since = Math.floor(new Date().getTime() / 1000);
|
||||||
}
|
}
|
||||||
return subLatest;
|
return subLatest;
|
||||||
}, [subject.type, subject.items]);
|
}, [pref, createSub]);
|
||||||
|
|
||||||
const latest = useSubscription(subRealtime, { leaveOpen: true });
|
const latest = useSubscription(subRealtime, { leaveOpen: true });
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
sub.ETags = new Set(trackingEvents);
|
sub.ETags = new Set(trackingEvents);
|
||||||
}
|
}
|
||||||
return sub ?? null;
|
return sub ?? null;
|
||||||
}, [trackingEvents]);
|
}, [trackingEvents, pref, subject.type]);
|
||||||
|
|
||||||
const others = useSubscription(subNext, { leaveOpen: true });
|
const others = useSubscription(subNext, { leaveOpen: true });
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
return parents;
|
return parents;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [trackingParentEvents]);
|
}, [trackingParentEvents, subject.type]);
|
||||||
|
|
||||||
const parent = useSubscription(subParents);
|
const parent = useSubscription(subParents);
|
||||||
|
|
||||||
@ -123,8 +123,10 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
if (main.store.notes.length > 0) {
|
if (main.store.notes.length > 0) {
|
||||||
setTrackingEvent(s => {
|
setTrackingEvent(s => {
|
||||||
let ids = main.store.notes.map(a => a.id);
|
let ids = main.store.notes.map(a => a.id);
|
||||||
let temp = new Set([...s, ...ids]);
|
if(ids.some(a => !s.includes(a))) {
|
||||||
return Array.from(temp);
|
return Array.from(new Set([...s, ...ids]));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
});
|
});
|
||||||
let reposts = main.store.notes
|
let reposts = main.store.notes
|
||||||
.filter(a => a.kind === EventKind.Repost && a.content === "")
|
.filter(a => a.kind === EventKind.Repost && a.content === "")
|
||||||
|
@ -6,6 +6,7 @@ import { default as NEvent } from "Nostr/Event";
|
|||||||
import { DefaultConnectTimeout } from "Const";
|
import { DefaultConnectTimeout } from "Const";
|
||||||
import { ConnectionStats } from "Nostr/ConnectionStats";
|
import { ConnectionStats } from "Nostr/ConnectionStats";
|
||||||
import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
||||||
|
import { RelayInfo } from "./RelayInfo";
|
||||||
|
|
||||||
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
|
||||||
|
|
||||||
@ -27,7 +28,8 @@ export type StateSnapshot = {
|
|||||||
events: {
|
events: {
|
||||||
received: number,
|
received: number,
|
||||||
send: number
|
send: number
|
||||||
}
|
},
|
||||||
|
info?: RelayInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
@ -36,6 +38,7 @@ export default class Connection {
|
|||||||
Pending: Subscriptions[];
|
Pending: Subscriptions[];
|
||||||
Subscriptions: Map<string, Subscriptions>;
|
Subscriptions: Map<string, Subscriptions>;
|
||||||
Settings: RelaySettings;
|
Settings: RelaySettings;
|
||||||
|
Info?: RelayInfo;
|
||||||
ConnectTimeout: number;
|
ConnectTimeout: number;
|
||||||
Stats: ConnectionStats;
|
Stats: ConnectionStats;
|
||||||
StateHooks: Map<string, CustomHook>;
|
StateHooks: Map<string, CustomHook>;
|
||||||
@ -56,7 +59,7 @@ export default class Connection {
|
|||||||
this.Stats = new ConnectionStats();
|
this.Stats = new ConnectionStats();
|
||||||
this.StateHooks = new Map();
|
this.StateHooks = new Map();
|
||||||
this.HasStateChange = true;
|
this.HasStateChange = true;
|
||||||
this.CurrentState = <StateSnapshot>{
|
this.CurrentState = {
|
||||||
connected: false,
|
connected: false,
|
||||||
disconnects: 0,
|
disconnects: 0,
|
||||||
avgLatency: 0,
|
avgLatency: 0,
|
||||||
@ -64,7 +67,7 @@ export default class Connection {
|
|||||||
received: 0,
|
received: 0,
|
||||||
send: 0
|
send: 0
|
||||||
}
|
}
|
||||||
};
|
} as StateSnapshot;
|
||||||
this.LastState = Object.freeze({ ...this.CurrentState });
|
this.LastState = Object.freeze({ ...this.CurrentState });
|
||||||
this.IsClosed = false;
|
this.IsClosed = false;
|
||||||
this.ReconnectTimer = null;
|
this.ReconnectTimer = null;
|
||||||
@ -72,7 +75,29 @@ export default class Connection {
|
|||||||
this.Connect();
|
this.Connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connect() {
|
async Connect() {
|
||||||
|
try {
|
||||||
|
if (this.Info === undefined) {
|
||||||
|
let u = new URL(this.Address);
|
||||||
|
let rsp = await fetch(`https://${u.host}`, {
|
||||||
|
headers: {
|
||||||
|
"accept": "application/nostr+json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (rsp.ok) {
|
||||||
|
let data = await rsp.json();
|
||||||
|
for (let [k, v] of Object.entries(data)) {
|
||||||
|
if (v === "unset" || v === "") {
|
||||||
|
data[k] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.Info = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not load relay information", e);
|
||||||
|
}
|
||||||
|
|
||||||
this.IsClosed = false;
|
this.IsClosed = false;
|
||||||
this.Socket = new WebSocket(this.Address);
|
this.Socket = new WebSocket(this.Address);
|
||||||
this.Socket.onopen = (e) => this.OnOpen(e);
|
this.Socket.onopen = (e) => this.OnOpen(e);
|
||||||
@ -259,6 +284,7 @@ export default class Connection {
|
|||||||
this.CurrentState.events.send = this.Stats.EventsSent;
|
this.CurrentState.events.send = this.Stats.EventsSent;
|
||||||
this.CurrentState.avgLatency = this.Stats.Latency.length > 0 ? (this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length) : 0;
|
this.CurrentState.avgLatency = this.Stats.Latency.length > 0 ? (this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length) : 0;
|
||||||
this.CurrentState.disconnects = this.Stats.Disconnects;
|
this.CurrentState.disconnects = this.Stats.Disconnects;
|
||||||
|
this.CurrentState.info = this.Info;
|
||||||
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
|
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
|
||||||
this.HasStateChange = true;
|
this.HasStateChange = true;
|
||||||
this._NotifyState();
|
this._NotifyState();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stats class for tracking metrics per connection
|
* Stats class for tracking metrics per connection
|
||||||
*/
|
*/
|
||||||
|
9
src/Nostr/RelayInfo.ts
Normal file
9
src/Nostr/RelayInfo.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface RelayInfo {
|
||||||
|
name?: string,
|
||||||
|
description?: string,
|
||||||
|
pubkey?: string,
|
||||||
|
contact?: string,
|
||||||
|
supported_nips?: number[],
|
||||||
|
software?: string,
|
||||||
|
version?: string
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ApiHost } from "Const";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import ZapButton from "Element/ZapButton";
|
import ZapButton from "Element/ZapButton";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
@ -24,7 +25,7 @@ const DonatePage = () => {
|
|||||||
const [splits, setSplits] = useState<Splits[]>([]);
|
const [splits, setSplits] = useState<Splits[]>([]);
|
||||||
|
|
||||||
async function loadSplits() {
|
async function loadSplits() {
|
||||||
let rsp = await fetch("https://api.snort.social/api/v1/revenue/splits");
|
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
|
||||||
if(rsp.ok) {
|
if(rsp.ok) {
|
||||||
setSplits(await rsp.json());
|
setSplits(await rsp.json());
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RecommendedFollows } from "Const";
|
import { ApiHost, RecommendedFollows } from "Const";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import FollowListBase from "Element/FollowListBase";
|
import FollowListBase from "Element/FollowListBase";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
@ -8,7 +8,7 @@ import { useSelector } from "react-redux";
|
|||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { bech32ToHex } from "Util";
|
import { bech32ToHex } from "Util";
|
||||||
|
|
||||||
const TwitterFollowsApi = "https://api.snort.social/api/v1/twitter/follows-for-nostr";
|
const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`;
|
||||||
|
|
||||||
export default function NewUserPage() {
|
export default function NewUserPage() {
|
||||||
const [twitterUsername, setTwitterUsername] = useState<string>("");
|
const [twitterUsername, setTwitterUsername] = useState<string>("");
|
||||||
@ -24,7 +24,7 @@ export default function NewUserPage() {
|
|||||||
const sortedTwitterFollows = useMemo(() => {
|
const sortedTwitterFollows = useMemo(() => {
|
||||||
return follows.map(a => bech32ToHex(a))
|
return follows.map(a => bech32ToHex(a))
|
||||||
.sort((a, b) => currentFollows.includes(a) ? 1 : -1);
|
.sort((a, b) => currentFollows.includes(a) ? 1 : -1);
|
||||||
}, [follows]);
|
}, [follows, currentFollows]);
|
||||||
|
|
||||||
async function loadFollows() {
|
async function loadFollows() {
|
||||||
setFollows([]);
|
setFollows([]);
|
||||||
|
@ -3,6 +3,7 @@ import SettingsIndex from "Pages/settings/Index";
|
|||||||
import Profile from "Pages/settings/Profile";
|
import Profile from "Pages/settings/Profile";
|
||||||
import Relay from "Pages/settings/Relays";
|
import Relay from "Pages/settings/Relays";
|
||||||
import Preferences from "Pages/settings/Preferences";
|
import Preferences from "Pages/settings/Preferences";
|
||||||
|
import RelayInfo from "Pages/settings/RelayInfo";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -26,7 +27,11 @@ export const SettingsRoutes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "relays",
|
path: "relays",
|
||||||
element: <Relay />
|
element: <Relay />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "relays/:addr",
|
||||||
|
element: <RelayInfo />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "preferences",
|
path: "preferences",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ApiHost } from "Const";
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
|
||||||
import './Verification.css'
|
import './Verification.css'
|
||||||
@ -6,7 +7,7 @@ export default function VerificationPage() {
|
|||||||
const services = [
|
const services = [
|
||||||
{
|
{
|
||||||
name: "Snort",
|
name: "Snort",
|
||||||
service: "https://api.snort.social/api/v1/n5sp",
|
service: `${ApiHost}/api/v1/n5sp`,
|
||||||
link: "https://snort.social/",
|
link: "https://snort.social/",
|
||||||
supportLink: "https://snort.social/help",
|
supportLink: "https://snort.social/help",
|
||||||
about: <>Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!</>
|
about: <>Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!</>
|
||||||
|
@ -31,7 +31,6 @@ export default function ProfileSettings() {
|
|||||||
const [about, setAbout] = useState<string>();
|
const [about, setAbout] = useState<string>();
|
||||||
const [website, setWebsite] = useState<string>();
|
const [website, setWebsite] = useState<string>();
|
||||||
const [nip05, setNip05] = useState<string>();
|
const [nip05, setNip05] = useState<string>();
|
||||||
const [lud06, setLud06] = useState<string>();
|
|
||||||
const [lud16, setLud16] = useState<string>();
|
const [lud16, setLud16] = useState<string>();
|
||||||
|
|
||||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture
|
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture
|
||||||
@ -45,7 +44,6 @@ export default function ProfileSettings() {
|
|||||||
setAbout(user.about);
|
setAbout(user.about);
|
||||||
setWebsite(user.website);
|
setWebsite(user.website);
|
||||||
setNip05(user.nip05);
|
setNip05(user.nip05);
|
||||||
setLud06(user.lud06);
|
|
||||||
setLud16(user.lud16);
|
setLud16(user.lud16);
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
57
src/Pages/settings/RelayInfo.tsx
Normal file
57
src/Pages/settings/RelayInfo.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
|
import useRelayState from "Feed/RelayState";
|
||||||
|
import { System } from "Nostr/System";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { removeRelay } from "State/Login";
|
||||||
|
import { parseId } from "Util";
|
||||||
|
|
||||||
|
const RelayInfo = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const addr: string = `wss://${params.addr}`;
|
||||||
|
|
||||||
|
const con = System.Sockets.get(addr) ?? System.Sockets.get(`${addr}/`);
|
||||||
|
const stats = useRelayState(con?.Address ?? addr);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>Relays</h3>
|
||||||
|
<div className="card">
|
||||||
|
<h3>{stats?.info?.name ?? addr}</h3>
|
||||||
|
<p>{stats?.info?.description}</p>
|
||||||
|
|
||||||
|
{stats?.info?.pubkey && (<>
|
||||||
|
<h4>Owner</h4>
|
||||||
|
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
|
||||||
|
</>)}
|
||||||
|
{stats?.info?.software && (<div className="flex">
|
||||||
|
<h4 className="f-grow">Software</h4>
|
||||||
|
<div className="flex f-col">
|
||||||
|
{stats.info.software.startsWith("http") ? <a href={stats.info.software} target="_blank" rel="noreferrer">{stats.info.software}</a> : <>{stats.info.software}</>}
|
||||||
|
<small>{!stats.info.version?.startsWith("v") && "v"}{stats.info.version}</small>
|
||||||
|
</div>
|
||||||
|
</div>)}
|
||||||
|
{stats?.info?.contact && (<div className="flex">
|
||||||
|
<h4 className="f-grow">Contact</h4>
|
||||||
|
<a href={`${stats.info.contact.startsWith("mailto:") ? "" : "mailto:"}${stats.info.contact}`} target="_blank" rel="noreferrer">{stats.info.contact}</a>
|
||||||
|
</div>)}
|
||||||
|
{stats?.info?.supported_nips && (<>
|
||||||
|
<h4>Supports</h4>
|
||||||
|
<div className="f-grow">
|
||||||
|
{stats.info.supported_nips.map(a => <span className="pill" onClick={() => navigate(`https://github.com/nostr-protocol/nips/blob/master/${a.toString().padStart(2, "0")}.md`)}>NIP-{a.toString().padStart(2, "0")}</span>)}
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
<div className="flex mt10 f-end">
|
||||||
|
<div className="btn error" onClick={() => {
|
||||||
|
dispatch(removeRelay(con!.Address));
|
||||||
|
navigate("/settings/relays")
|
||||||
|
}}>Remove</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelayInfo;
|
@ -52,7 +52,7 @@ const RelaySettingsPage = () => {
|
|||||||
<div className="flex f-col">
|
<div className="flex f-col">
|
||||||
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex actions">
|
<div className="flex mt10">
|
||||||
<div className="f-grow"></div>
|
<div className="f-grow"></div>
|
||||||
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import * as secp from '@noble/secp256k1';
|
import * as secp from '@noble/secp256k1';
|
||||||
import { DefaultRelays } from 'Const';
|
import { DefaultRelays } from 'Const';
|
||||||
import { HexKey, RawEvent, TaggedRawEvent } from 'Nostr';
|
import { HexKey, TaggedRawEvent } from 'Nostr';
|
||||||
import { RelaySettings } from 'Nostr/Connection';
|
import { RelaySettings } from 'Nostr/Connection';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
const PrivateKeyItem = "secret";
|
const PrivateKeyItem = "secret";
|
||||||
const PublicKeyItem = "pubkey";
|
const PublicKeyItem = "pubkey";
|
||||||
@ -182,7 +181,7 @@ const LoginSlice = createSlice({
|
|||||||
let filtered = new Map<string, RelaySettings>();
|
let filtered = new Map<string, RelaySettings>();
|
||||||
for (let [k, v] of Object.entries(relays)) {
|
for (let [k, v] of Object.entries(relays)) {
|
||||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||||
filtered.set(k, <RelaySettings>v);
|
filtered.set(k, v as RelaySettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import { HexKey, RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
|
|
||||||
export async function openFile(): Promise<File | undefined> {
|
export async function openFile(): Promise<File | undefined> {
|
||||||
@ -65,7 +65,7 @@ export function eventLink(hex: u256) {
|
|||||||
* @param {string} hex
|
* @param {string} hex
|
||||||
*/
|
*/
|
||||||
export function hexToBech32(hrp: string, hex: string) {
|
export function hexToBech32(hrp: string, hex: string) {
|
||||||
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 != 0) {
|
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,18 +222,18 @@ textarea:placeholder {
|
|||||||
align-items: flex-start !important;
|
align-items: flex-start !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.f-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.w-max {
|
.w-max {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width: -moz-available;
|
width: stretch;
|
||||||
width: -webkit-fill-available;
|
|
||||||
width: fill-available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-max-w {
|
.w-max-w {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-width: -moz-available;
|
max-width: stretch;
|
||||||
max-width: -webkit-fill-available;
|
|
||||||
max-width: fill-available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -263,7 +263,7 @@ div.form-group>div:nth-child(1) {
|
|||||||
div.form-group>div:nth-child(2) {
|
div.form-group>div:nth-child(2) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.form-group>div:nth-child(2) input {
|
div.form-group>div:nth-child(2) input {
|
||||||
|
Loading…
Reference in New Issue
Block a user