feat: WoT filter
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
@ -41,7 +42,7 @@ export function Note(props: NoteProps) {
|
|||||||
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
|
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
|
||||||
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
|
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
|
||||||
const [showTranslation, setShowTranslation] = useState(true);
|
const [showTranslation, setShowTranslation] = useState(true);
|
||||||
const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
|
const [translated, setTranslated] = useState<NoteTranslation | null>(translationCache.get(ev.id));
|
||||||
const cachedSetTranslated = useCallback(
|
const cachedSetTranslated = useCallback(
|
||||||
(translation: NoteTranslation) => {
|
(translation: NoteTranslation) => {
|
||||||
translationCache.set(ev.id, translation);
|
translationCache.set(ev.id, translation);
|
||||||
@ -54,7 +55,9 @@ export function Note(props: NoteProps) {
|
|||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
if (setSeenAtInView) {
|
if (setSeenAtInView) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) });
|
if (Relay instanceof WorkerRelayInterface) {
|
||||||
|
Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) });
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
|
@ -9,6 +9,7 @@ import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
|||||||
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
||||||
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import useModeration from "@/Hooks/useModeration";
|
||||||
import usePreferences from "@/Hooks/usePreferences";
|
import usePreferences from "@/Hooks/usePreferences";
|
||||||
|
|
||||||
export interface NoteFooterProps {
|
export interface NoteFooterProps {
|
||||||
@ -20,9 +21,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const { ev } = props;
|
const { ev } = props;
|
||||||
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
|
const { isMuted } = useModeration();
|
||||||
|
|
||||||
const related = useReactions("reactions", link);
|
const related = useReactions("reactions", link);
|
||||||
const { replies, reactions, zaps, reposts } = useEventReactions(link, related);
|
const { replies, reactions, zaps, reposts } = useEventReactions(
|
||||||
|
link,
|
||||||
|
related.filter(a => !isMuted(a.pubkey)),
|
||||||
|
);
|
||||||
const { positive } = reactions;
|
const { positive } = reactions;
|
||||||
|
|
||||||
const readonly = useLogin(s => s.readonly);
|
const readonly = useLogin(s => s.readonly);
|
||||||
|
@ -1,21 +1,15 @@
|
|||||||
import "../EventComponent.css";
|
import classNames from "classnames";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
|
||||||
|
|
||||||
interface NoteGhostProps {
|
interface NoteGhostProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
link: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteGhost(props: NoteGhostProps) {
|
export default function NoteGhost(props: NoteGhostProps) {
|
||||||
const className = `note card ${props.className ?? ""}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={classNames("p bb", props.className)}>
|
||||||
<div className="header">
|
<FormattedMessage defaultMessage="Loading note: {id}" values={{ id: props.link }} />
|
||||||
<ProfileImage pubkey="" />
|
|
||||||
</div>
|
|
||||||
<div className="body">{props.children}</div>
|
|
||||||
<div className="footer"></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,6 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
waitUntilInView={false}
|
waitUntilInView={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return <NoteGhost className={className}>Loading thread root.. ({thread.data?.length} notes loaded)</NoteGhost>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,16 +73,18 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
function renderCurrent() {
|
function renderCurrent() {
|
||||||
if (thread.current) {
|
if (thread.current) {
|
||||||
const note = thread.data.find(n => n.id === thread.current);
|
const note = thread.data.find(n => n.id === thread.current);
|
||||||
return (
|
if (note) {
|
||||||
note && (
|
return (
|
||||||
<Note
|
<Note
|
||||||
data={note}
|
data={note}
|
||||||
options={{ showReactionsLink: true, showMediaSpotlight: true }}
|
options={{ showReactionsLink: true, showMediaSpotlight: true }}
|
||||||
threadChains={thread.chains}
|
threadChains={thread.chains}
|
||||||
onClick={navigateThread}
|
onClick={navigateThread}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
);
|
} else {
|
||||||
|
return <NoteGhost link={thread.current} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,6 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
|
|
||||||
const parentText = formatMessage({
|
const parentText = formatMessage({
|
||||||
defaultMessage: "Parent",
|
defaultMessage: "Parent",
|
||||||
id: "ADmfQT",
|
|
||||||
description: "Link to parent note in thread",
|
description: "Link to parent note in thread",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -134,10 +133,15 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
{thread.root && renderRoot(thread.root)}
|
{thread.root && renderRoot(thread.root)}
|
||||||
{thread.root && renderChain(chainKey(thread.root))}
|
{thread.root && renderChain(chainKey(thread.root))}
|
||||||
{!thread.root && renderCurrent()}
|
{!thread.root && renderCurrent()}
|
||||||
{!thread.root && !thread.current && (
|
{thread.mutedData.length > 0 && (
|
||||||
<NoteGhost>
|
<div className="p br b mx-2 my-3 bg-gray-ultradark text-gray-light font-medium cursor-pointer">
|
||||||
<FormattedMessage defaultMessage="Looking up thread..." />
|
<FormattedMessage
|
||||||
</NoteGhost>
|
defaultMessage="{n} notes have been muted"
|
||||||
|
values={{
|
||||||
|
n: thread.mutedData.length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
import { UserRelays } from "@/Cache";
|
import { UserRelays } from "@/Cache";
|
||||||
|
import useWoT from "@/Hooks/useWoT";
|
||||||
import { getRelayName } from "@/Utils";
|
import { getRelayName } from "@/Utils";
|
||||||
|
|
||||||
export function UserDebug({ pubkey }: { pubkey: string }) {
|
export function UserDebug({ pubkey }: { pubkey: string }) {
|
||||||
const profile = useUserProfile(pubkey);
|
const profile = useUserProfile(pubkey);
|
||||||
const relays = UserRelays.getFromCache(pubkey);
|
const relays = UserRelays.getFromCache(pubkey);
|
||||||
|
const wot = useWoT();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<div className="flex flex-col overflow-wrap">
|
<div className="flex flex-col overflow-wrap">
|
||||||
|
<div className="flex justify-between gap-1">
|
||||||
|
<div>WoT Distance:</div>
|
||||||
|
<div>{wot.followDistance(pubkey)}</div>
|
||||||
|
</div>
|
||||||
{Object.entries(profile ?? {}).map(([k, v]) => {
|
{Object.entries(profile ?? {}).map(([k, v]) => {
|
||||||
let vv = <div>{v}</div>;
|
let vv = <div>{v}</div>;
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent, ToNostrEventTag, Un
|
|||||||
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
|
import useWoT from "./useWoT";
|
||||||
|
|
||||||
export class MutedWordTag implements ToNostrEventTag {
|
export class MutedWordTag implements ToNostrEventTag {
|
||||||
constructor(readonly word: string) {}
|
constructor(readonly word: string) {}
|
||||||
equals(other: ToNostrEventTag): boolean {
|
equals(other: ToNostrEventTag): boolean {
|
||||||
@ -16,10 +18,12 @@ export class MutedWordTag implements ToNostrEventTag {
|
|||||||
|
|
||||||
export default function useModeration() {
|
export default function useModeration() {
|
||||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
||||||
|
const wot = useWoT();
|
||||||
|
|
||||||
function isMuted(id: string) {
|
function isMuted(pubkey: string) {
|
||||||
const link = NostrLink.publicKey(id);
|
const link = NostrLink.publicKey(pubkey);
|
||||||
return state.muted.some(a => a.equals(link));
|
const distance = wot.followDistance(pubkey);
|
||||||
|
return state.muted.some(a => a.equals(link)) || (state.appdata?.preferences.muteWithWoT && distance > 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unmute(id: string) {
|
async function unmute(id: string) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { TaggedNostrEvent } from "@snort/system";
|
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||||
import { socialGraphInstance } from "@snort/system/dist/SocialGraph/SocialGraph";
|
|
||||||
|
|
||||||
export default function useWoT() {
|
export default function useWoT() {
|
||||||
const sg = socialGraphInstance;
|
|
||||||
return {
|
return {
|
||||||
sortEvents: (events: Array<TaggedNostrEvent>) =>
|
sortEvents: (events: Array<TaggedNostrEvent>) =>
|
||||||
events.sort((a, b) => sg.getFollowDistance(a.pubkey) - sg.getFollowDistance(b.pubkey)),
|
events.sort(
|
||||||
followDistance: sg.getFollowDistance,
|
(a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey),
|
||||||
|
),
|
||||||
|
followDistance: (pk: string) => socialGraphInstance.getFollowDistance(pk),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -182,6 +182,40 @@ const PreferencesPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between w-max">
|
||||||
|
<div className="flex flex-col g8">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="WoT Filter" />
|
||||||
|
</h4>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Mute notes from people who are outside your web of trust" />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={pref.muteWithWoT}
|
||||||
|
onChange={e => setPref({ ...pref, muteWithWoT: e.target.checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex flex-col g8">
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Hide muted notes" />
|
||||||
|
</h4>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Muted notes will not be shown" />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={pref.hideMutedNotes}
|
||||||
|
onChange={e => setPref({ ...pref, hideMutedNotes: e.target.checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between w-max">
|
<div className="flex justify-between w-max">
|
||||||
<div className="flex flex-col g8">
|
<div className="flex flex-col g8">
|
||||||
<h4>
|
<h4>
|
||||||
@ -471,23 +505,7 @@ const PreferencesPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-col g8">
|
|
||||||
<h4>
|
|
||||||
<FormattedMessage defaultMessage="Hide muted notes" />
|
|
||||||
</h4>
|
|
||||||
<small>
|
|
||||||
<FormattedMessage defaultMessage="Muted notes will not be shown" />
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={pref.hideMutedNotes}
|
|
||||||
onChange={e => setPref({ ...pref, hideMutedNotes: e.target.checked })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AsyncButton onClick={() => update(pref)}>
|
<AsyncButton onClick={() => update(pref)}>
|
||||||
<FormattedMessage defaultMessage="Save" />
|
<FormattedMessage defaultMessage="Save" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
@ -102,6 +102,11 @@ export interface UserPreferences {
|
|||||||
* Show posts with content warning
|
* Show posts with content warning
|
||||||
*/
|
*/
|
||||||
showContentWarningPosts: boolean;
|
showContentWarningPosts: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute notes outside your WoT
|
||||||
|
*/
|
||||||
|
muteWithWoT: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultPreferences = {
|
export const DefaultPreferences = {
|
||||||
@ -122,5 +127,6 @@ export const DefaultPreferences = {
|
|||||||
checkSigs: true,
|
checkSigs: true,
|
||||||
autoTranslate: true,
|
autoTranslate: true,
|
||||||
hideMutedNotes: false,
|
hideMutedNotes: false,
|
||||||
|
muteWithWoT: false,
|
||||||
showContentWarningPosts: false,
|
showContentWarningPosts: false,
|
||||||
} as UserPreferences;
|
} as UserPreferences;
|
||||||
|
@ -7,6 +7,7 @@ export interface ThreadContextState {
|
|||||||
root?: TaggedNostrEvent;
|
root?: TaggedNostrEvent;
|
||||||
chains: Map<string, Array<TaggedNostrEvent>>;
|
chains: Map<string, Array<TaggedNostrEvent>>;
|
||||||
data: Array<TaggedNostrEvent>;
|
data: Array<TaggedNostrEvent>;
|
||||||
|
mutedData: Array<TaggedNostrEvent>;
|
||||||
setCurrent: (i: string) => void;
|
setCurrent: (i: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { NostrLink, TaggedNostrEvent, u256 } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { ReactNode, useMemo, useState } from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
@ -11,52 +11,53 @@ import { ThreadContext, ThreadContextState } from "@/Utils/Thread/ThreadContext"
|
|||||||
export function ThreadContextWrapper({ link, children }: { link: NostrLink; children?: ReactNode }) {
|
export function ThreadContextWrapper({ link, children }: { link: NostrLink; children?: ReactNode }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [currentId, setCurrentId] = useState(unwrap(link.toEventTag())[1]);
|
const [currentId, setCurrentId] = useState(unwrap(link.toEventTag())[1]);
|
||||||
const feed = useThreadFeed(link);
|
const feedData = useThreadFeed(link);
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
|
|
||||||
const chains = useMemo(() => {
|
function threadChains(notes: Array<TaggedNostrEvent>) {
|
||||||
const chains = new Map<u256, Array<TaggedNostrEvent>>();
|
const chains = new Map<string, Array<TaggedNostrEvent>>();
|
||||||
if (feed) {
|
notes
|
||||||
feed
|
.filter(a => !isMuted(a.pubkey))
|
||||||
?.filter(a => !isMuted(a.pubkey))
|
.forEach(v => {
|
||||||
.forEach(v => {
|
const replyTo = replyChainKey(v);
|
||||||
const replyTo = replyChainKey(v);
|
if (replyTo) {
|
||||||
if (replyTo) {
|
if (!chains.has(replyTo)) {
|
||||||
if (!chains.has(replyTo)) {
|
chains.set(replyTo, [v]);
|
||||||
chains.set(replyTo, [v]);
|
} else {
|
||||||
} else {
|
unwrap(chains.get(replyTo)).push(v);
|
||||||
unwrap(chains.get(replyTo)).push(v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
return chains;
|
return chains;
|
||||||
}, [feed, isMuted]);
|
}
|
||||||
|
|
||||||
// Root is the parent of the current note or the current note if its a root note or the root of the thread
|
// Root is the parent of the current note or
|
||||||
|
// the current note if its a root note or
|
||||||
|
// the root of the thread
|
||||||
const root = useMemo(() => {
|
const root = useMemo(() => {
|
||||||
const currentNote =
|
const currentNote =
|
||||||
feed?.find(a => chainKey(a) === currentId) ??
|
feedData.find(a => chainKey(a) === currentId) ??
|
||||||
(location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
|
(location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
|
||||||
if (currentNote) {
|
if (currentNote) {
|
||||||
const key = replyChainKey(currentNote);
|
const key = replyChainKey(currentNote);
|
||||||
if (key) {
|
if (key) {
|
||||||
return feed?.find(a => chainKey(a) === key);
|
return feedData.find(a => chainKey(a) === key);
|
||||||
} else {
|
} else {
|
||||||
return currentNote;
|
return currentNote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [feed, location.state, currentId]);
|
}, [feedData, location.state, currentId]);
|
||||||
|
|
||||||
const ctxValue = useMemo<ThreadContextState>(() => {
|
const ctxValue = useMemo<ThreadContextState>(() => {
|
||||||
return {
|
return {
|
||||||
current: currentId,
|
current: currentId,
|
||||||
root,
|
root,
|
||||||
chains,
|
chains: threadChains(feedData.filter(a => !isMuted(a.pubkey))),
|
||||||
data: feed,
|
data: feedData,
|
||||||
|
mutedData: feedData.filter(a => isMuted(a.pubkey)),
|
||||||
setCurrent: v => setCurrentId(v),
|
setCurrent: v => setCurrentId(v),
|
||||||
};
|
};
|
||||||
}, [currentId, root, chains, feed]);
|
}, [currentId, root, feedData]);
|
||||||
|
|
||||||
return <ThreadContext.Provider value={ctxValue}>{children}</ThreadContext.Provider>;
|
return <ThreadContext.Provider value={ctxValue}>{children}</ThreadContext.Provider>;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ import "./index.css";
|
|||||||
import "@szhsin/react-menu/dist/index.css";
|
import "@szhsin/react-menu/dist/index.css";
|
||||||
import "@/assets/fonts/inter.css";
|
import "@/assets/fonts/inter.css";
|
||||||
|
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow, unixNowMs } from "@snort/shared";
|
||||||
|
import { socialGraphInstance } from "@snort/system";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import * as ReactDOM from "react-dom/client";
|
import * as ReactDOM from "react-dom/client";
|
||||||
@ -60,7 +61,15 @@ async function initSite() {
|
|||||||
|
|
||||||
const login = LoginStore.snapshot();
|
const login = LoginStore.snapshot();
|
||||||
preload(login.state.follows).then(async () => {
|
preload(login.state.follows).then(async () => {
|
||||||
queueMicrotask(() => System.PreloadSocialGraph(login.state.follows));
|
queueMicrotask(async () => {
|
||||||
|
const start = unixNowMs();
|
||||||
|
await System.PreloadSocialGraph(login.state.follows);
|
||||||
|
console.debug(
|
||||||
|
`Social graph loaded in ${(unixNowMs() - start).toFixed(2)}ms, followDistances=${
|
||||||
|
socialGraphInstance.followDistanceByUser.size
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
for (const ev of UserCache.snapshot()) {
|
for (const ev of UserCache.snapshot()) {
|
||||||
try {
|
try {
|
||||||
|
@ -342,6 +342,9 @@
|
|||||||
"7hp70g": {
|
"7hp70g": {
|
||||||
"defaultMessage": "NIP-05"
|
"defaultMessage": "NIP-05"
|
||||||
},
|
},
|
||||||
|
"7nAz/z": {
|
||||||
|
"defaultMessage": "Mute notes from people who are outside your web of trust"
|
||||||
|
},
|
||||||
"7pFGAQ": {
|
"7pFGAQ": {
|
||||||
"defaultMessage": "Close Relays"
|
"defaultMessage": "Close Relays"
|
||||||
},
|
},
|
||||||
@ -753,9 +756,6 @@
|
|||||||
"J2HeQ+": {
|
"J2HeQ+": {
|
||||||
"defaultMessage": "Use commas to separate words e.g. word1, word2, word3"
|
"defaultMessage": "Use commas to separate words e.g. word1, word2, word3"
|
||||||
},
|
},
|
||||||
"JA+tz3": {
|
|
||||||
"defaultMessage": "Looking up thread..."
|
|
||||||
},
|
|
||||||
"JCIgkj": {
|
"JCIgkj": {
|
||||||
"defaultMessage": "Username"
|
"defaultMessage": "Username"
|
||||||
},
|
},
|
||||||
@ -1033,6 +1033,9 @@
|
|||||||
"Rs4kCE": {
|
"Rs4kCE": {
|
||||||
"defaultMessage": "Bookmark"
|
"defaultMessage": "Bookmark"
|
||||||
},
|
},
|
||||||
|
"S/NV2G": {
|
||||||
|
"defaultMessage": "Loading note: {id}"
|
||||||
|
},
|
||||||
"SFuk1v": {
|
"SFuk1v": {
|
||||||
"defaultMessage": "Permissions"
|
"defaultMessage": "Permissions"
|
||||||
},
|
},
|
||||||
@ -1389,6 +1392,9 @@
|
|||||||
"d+6YsV": {
|
"d+6YsV": {
|
||||||
"defaultMessage": "Lists to mute:"
|
"defaultMessage": "Lists to mute:"
|
||||||
},
|
},
|
||||||
|
"d0qim7": {
|
||||||
|
"defaultMessage": "WoT Filter"
|
||||||
|
},
|
||||||
"d2ebEu": {
|
"d2ebEu": {
|
||||||
"defaultMessage": "Not Subscribed to Push"
|
"defaultMessage": "Not Subscribed to Push"
|
||||||
},
|
},
|
||||||
@ -1713,6 +1719,9 @@
|
|||||||
"lvlPhZ": {
|
"lvlPhZ": {
|
||||||
"defaultMessage": "Pay Invoice"
|
"defaultMessage": "Pay Invoice"
|
||||||
},
|
},
|
||||||
|
"mCEKiZ": {
|
||||||
|
"defaultMessage": "{n} notes have been muted"
|
||||||
|
},
|
||||||
"mErPop": {
|
"mErPop": {
|
||||||
"defaultMessage": "It looks like you dont have any, check {link} to buy one!"
|
"defaultMessage": "It looks like you dont have any, check {link} to buy one!"
|
||||||
},
|
},
|
||||||
|
@ -113,6 +113,7 @@
|
|||||||
"7UOvbT": "Offline",
|
"7UOvbT": "Offline",
|
||||||
"7YkSA2": "Community Leader",
|
"7YkSA2": "Community Leader",
|
||||||
"7hp70g": "NIP-05",
|
"7hp70g": "NIP-05",
|
||||||
|
"7nAz/z": "Mute notes from people who are outside your web of trust",
|
||||||
"7pFGAQ": "Close Relays",
|
"7pFGAQ": "Close Relays",
|
||||||
"8/vBbP": "Reposts ({n})",
|
"8/vBbP": "Reposts ({n})",
|
||||||
"89q5wc": "Confirm Reposts",
|
"89q5wc": "Confirm Reposts",
|
||||||
@ -249,7 +250,6 @@
|
|||||||
"J+dIsA": "Subscriptions",
|
"J+dIsA": "Subscriptions",
|
||||||
"J1iLmb": "Notifications Not Allowed",
|
"J1iLmb": "Notifications Not Allowed",
|
||||||
"J2HeQ+": "Use commas to separate words e.g. word1, word2, word3",
|
"J2HeQ+": "Use commas to separate words e.g. word1, word2, word3",
|
||||||
"JA+tz3": "Looking up thread...",
|
|
||||||
"JCIgkj": "Username",
|
"JCIgkj": "Username",
|
||||||
"JGrt9q": "Send sats to {name}",
|
"JGrt9q": "Send sats to {name}",
|
||||||
"JHEHCk": "Zaps ({n})",
|
"JHEHCk": "Zaps ({n})",
|
||||||
@ -342,6 +342,7 @@
|
|||||||
"RmxSZo": "Data Vending Machines",
|
"RmxSZo": "Data Vending Machines",
|
||||||
"RoOyAh": "Relays",
|
"RoOyAh": "Relays",
|
||||||
"Rs4kCE": "Bookmark",
|
"Rs4kCE": "Bookmark",
|
||||||
|
"S/NV2G": "Loading note: {id}",
|
||||||
"SFuk1v": "Permissions",
|
"SFuk1v": "Permissions",
|
||||||
"SLZGPn": "Enter a pin to encrypt your private key, you must enter this pin every time you open {site}.",
|
"SLZGPn": "Enter a pin to encrypt your private key, you must enter this pin every time you open {site}.",
|
||||||
"SMO+on": "Send zap to {name}",
|
"SMO+on": "Send zap to {name}",
|
||||||
@ -460,6 +461,7 @@
|
|||||||
"cw1Ftc": "Live Activities",
|
"cw1Ftc": "Live Activities",
|
||||||
"cyR7Kh": "Back",
|
"cyR7Kh": "Back",
|
||||||
"d+6YsV": "Lists to mute:",
|
"d+6YsV": "Lists to mute:",
|
||||||
|
"d0qim7": "WoT Filter",
|
||||||
"d2ebEu": "Not Subscribed to Push",
|
"d2ebEu": "Not Subscribed to Push",
|
||||||
"d7d0/x": "LN Address",
|
"d7d0/x": "LN Address",
|
||||||
"dK2CcV": "The public key is like your username, you can share it with anyone.",
|
"dK2CcV": "The public key is like your username, you can share it with anyone.",
|
||||||
@ -568,6 +570,7 @@
|
|||||||
"lnaT9F": "Following {n}",
|
"lnaT9F": "Following {n}",
|
||||||
"lsNFM1": "Click to load content from {link}",
|
"lsNFM1": "Click to load content from {link}",
|
||||||
"lvlPhZ": "Pay Invoice",
|
"lvlPhZ": "Pay Invoice",
|
||||||
|
"mCEKiZ": "{n} notes have been muted",
|
||||||
"mErPop": "It looks like you dont have any, check {link} to buy one!",
|
"mErPop": "It looks like you dont have any, check {link} to buy one!",
|
||||||
"mFtdYh": "{type} Worker Relay",
|
"mFtdYh": "{type} Worker Relay",
|
||||||
"mKAr6h": "Follow all",
|
"mKAr6h": "Follow all",
|
||||||
|
Reference in New Issue
Block a user