chore: Update translations
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Kieran 2023-09-14 18:45:29 +00:00
parent a8f000d9d8
commit 9c05b6a101
21 changed files with 609 additions and 567 deletions

View File

@ -3,8 +3,12 @@ import { orderDescending } from "SnortUtils";
import Note from "../Note"; import Note from "../Note";
export default function Articles() { export default function Articles() {
const data = useArticles(); const data = useArticles();
return <> return (
{orderDescending(data.data ?? []).map(a => <Note data={a} key={a.id} related={[]} />)} <>
{orderDescending(data.data ?? []).map(a => (
<Note data={a} key={a.id} related={[]} />
))}
</> </>
} );
}

View File

@ -1,12 +1,12 @@
nav.deck { nav.deck {
width: 48px; width: 48px;
height: calc(100vh - 20px); height: calc(100vh - 20px);
padding: 10px 8px; padding: 10px 8px;
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
text-align: center; text-align: center;
} }
nav.deck .avatar { nav.deck .avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
} }

View File

@ -7,36 +7,35 @@ import { Link } from "react-router-dom";
import { profileLink } from "SnortUtils"; import { profileLink } from "SnortUtils";
export function DeckNav() { export function DeckNav() {
const { publicKey } = useLogin(); const { publicKey } = useLogin();
const profile = useUserProfile(publicKey); const profile = useUserProfile(publicKey);
const unreadDms = 0; const unreadDms = 0;
const hasNotifications = false; const hasNotifications = false;
return <nav className="deck flex-column f-space"> return (
<div className="flex-column f-center g24"> <nav className="deck flex-column f-space">
<Link className="btn" to="/messages"> <div className="flex-column f-center g24">
<Icon name="mail" size={24} /> <Link className="btn" to="/messages">
{unreadDms > 0 && <span className="has-unread"></span>} <Icon name="mail" size={24} />
</Link> {unreadDms > 0 && <span className="has-unread"></span>}
<Link className="btn" to="/notifications"> </Link>
<Icon name="bell-02" size={24} /> <Link className="btn" to="/notifications">
{hasNotifications && <span className="has-unread"></span>} <Icon name="bell-02" size={24} />
</Link> {hasNotifications && <span className="has-unread"></span>}
</div> </Link>
<div className="flex-column f-center g16"> </div>
<Link className="btn" to="/"> <div className="flex-column f-center g16">
<Icon name="grid-01" size={24} /> <Link className="btn" to="/">
</Link> <Icon name="grid-01" size={24} />
<Link className="btn" to="/settings"> </Link>
<Icon name="settings-02" size={24} /> <Link className="btn" to="/settings">
</Link> <Icon name="settings-02" size={24} />
<Link to={profileLink(publicKey ?? "")}> </Link>
<Avatar <Link to={profileLink(publicKey ?? "")}>
pubkey={publicKey ?? ""} <Avatar pubkey={publicKey ?? ""} user={profile} />
user={profile} </Link>
/> </div>
</Link>
</div>
</nav> </nav>
} );
}

View File

@ -3,7 +3,17 @@ import React, { useMemo, useState, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { TaggedNostrEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap, tagToNostrLink, createNostrLinkToEvent } from "@snort/system"; import {
TaggedNostrEvent,
HexKey,
EventKind,
NostrPrefix,
Lists,
EventExt,
parseZap,
tagToNostrLink,
createNostrLinkToEvent,
} from "@snort/system";
import { System } from "index"; import { System } from "index";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
@ -200,19 +210,22 @@ export function NoteInner(props: NoteProps) {
const image = findTag(ev, "image"); const image = findTag(ev, "image");
return ( return (
<div className="long-form-note"> <div className="long-form-note">
<h3> <h3>{title}</h3>
{title}
</h3>
<div className="text"> <div className="text">
<p> <p>{summary}</p>
{summary} <Text
</p> id={ev.id}
<Text id={ev.id} content={ev.content} tags={ev.tags} creator={ev.pubkey} depth={props.depth} truncate={255} disableLinkPreview={true} /> content={ev.content}
tags={ev.tags}
creator={ev.pubkey}
depth={props.depth}
truncate={255}
disableLinkPreview={true}
/>
{image && <ProxyImg src={image} />} {image && <ProxyImg src={image} />}
</div> </div>
</div> </div>
) );
} else { } else {
const body = ev?.content ?? ""; const body = ev?.content ?? "";
return ( return (
@ -226,7 +239,7 @@ export function NoteInner(props: NoteProps) {
/> />
); );
} }
} };
const transformBody = () => { const transformBody = () => {
if (deletions?.length > 0) { if (deletions?.length > 0) {
@ -297,7 +310,11 @@ export function NoteInner(props: NoteProps) {
const maxMentions = 2; const maxMentions = 2;
const replyTo = thread?.replyTo ?? thread?.root; const replyTo = thread?.replyTo ?? thread?.root;
const replyLink = replyTo ? tagToNostrLink([replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0)) : undefined; const replyLink = replyTo
? tagToNostrLink(
[replyTo.key, replyTo.value ?? "", replyTo.relay ?? "", replyTo.marker ?? ""].filter(a => a.length > 0),
)
: undefined;
const mentions: { pk: string; name: string; link: ReactNode }[] = []; const mentions: { pk: string; name: string; link: ReactNode }[] = [];
for (const pk of thread?.pubKeys ?? []) { for (const pk of thread?.pubKeys ?? []) {
const u = UserCache.getFromCache(pk); const u = UserCache.getFromCache(pk);
@ -330,11 +347,7 @@ export function NoteInner(props: NoteProps) {
{pubMentions} {others} {pubMentions} {others}
</> </>
) : ( ) : (
replyLink && ( replyLink && <Link to={`/e/${replyLink.encode()}`}>{replyLink.encode().substring(0, 12)}</Link>
<Link to={`/e/${replyLink.encode()}`}>
{replyLink.encode().substring(0, 12)}
</Link>
)
)} )}
</div> </div>
); );
@ -418,7 +431,7 @@ export function NoteInner(props: NoteProps) {
{options.showContextMenu && ( {options.showContextMenu && (
<NoteContextMenu <NoteContextMenu
ev={ev} ev={ev}
react={async () => { }} react={async () => {}}
onTranslated={t => setTranslated(t)} onTranslated={t => setTranslated(t)}
setShowReactions={setShowReactions} setShowReactions={setShowReactions}
/> />

View File

@ -1,18 +1,17 @@
.root-type { .root-type {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.root-type > button { .root-type > button {
background: white; background: white;
color: black; color: black;
font-size: 16px; font-size: 16px;
padding: 10px 16px; padding: 10px 16px;
border-radius: 1000px; border-radius: 1000px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
} }

View File

@ -7,145 +7,150 @@ import { FormattedMessage } from "react-intl";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
export type RootTab =
export type RootTab = "following" | "conversations" | "trending-notes" | "trending-people" | "suggested" | "tags" | "global"; | "following"
| "conversations"
| "trending-notes"
| "trending-people"
| "suggested"
| "tags"
| "global";
export function RootTabs({ base }: { base?: string }) { export function RootTabs({ base }: { base?: string }) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { publicKey: pubKey, tags } = useLogin(); const { publicKey: pubKey, tags } = useLogin();
const [rootType, setRootType] = useState<RootTab>("following"); const [rootType, setRootType] = useState<RootTab>("following");
const menuItems = [ const menuItems = [
{ {
tab: "following", tab: "following",
path: `${base}/notes`, path: `${base}/notes`,
show: Boolean(pubKey), show: Boolean(pubKey),
element: ( element: (
<> <>
<Icon name="user-v2" /> <Icon name="user-v2" />
<FormattedMessage defaultMessage="Following" /> <FormattedMessage defaultMessage="Following" />
</> </>
), ),
}, },
{ {
tab: "trending-notes", tab: "trending-notes",
path: `${base}/trending/notes`, path: `${base}/trending/notes`,
show: true, show: true,
element: ( element: (
<> <>
<Icon name="fire" /> <Icon name="fire" />
<FormattedMessage defaultMessage="Trending Notes" /> <FormattedMessage defaultMessage="Trending Notes" />
</> </>
), ),
}, },
{ {
tab: "conversations", tab: "conversations",
path: `${base}/conversations`, path: `${base}/conversations`,
show: Boolean(pubKey), show: Boolean(pubKey),
element: ( element: (
<> <>
<Icon name="message-chat-circle" /> <Icon name="message-chat-circle" />
<FormattedMessage defaultMessage="Conversations" /> <FormattedMessage defaultMessage="Conversations" />
</> </>
), ),
}, },
{ {
tab: "trending-people", tab: "trending-people",
path: `${base}/trending/people`, path: `${base}/trending/people`,
show: true, show: true,
element: ( element: (
<> <>
<Icon name="user-up" /> <Icon name="user-up" />
<FormattedMessage defaultMessage="Trending People" /> <FormattedMessage defaultMessage="Trending People" />
</> </>
), ),
}, },
{ {
tab: "suggested", tab: "suggested",
path: `${base}/suggested`, path: `${base}/suggested`,
show: Boolean(pubKey), show: Boolean(pubKey),
element: ( element: (
<> <>
<Icon name="thumbs-up" /> <Icon name="thumbs-up" />
<FormattedMessage defaultMessage="Suggested Follows" /> <FormattedMessage defaultMessage="Suggested Follows" />
</> </>
), ),
}, },
{ {
tab: "global", tab: "global",
path: `${base}/global`, path: `${base}/global`,
show: true, show: true,
element: ( element: (
<> <>
<Icon name="globe" /> <Icon name="globe" />
<FormattedMessage defaultMessage="Global" /> <FormattedMessage defaultMessage="Global" />
</> </>
), ),
}, },
] as Array<{ ] as Array<{
tab: RootTab; tab: RootTab;
path: string; path: string;
show: boolean; show: boolean;
element: ReactNode; element: ReactNode;
}>; }>;
useEffect(() => { useEffect(() => {
const currentTab = menuItems.find(a => a.path === location.pathname)?.tab; const currentTab = menuItems.find(a => a.path === location.pathname)?.tab;
if (currentTab) { if (currentTab) {
setRootType(currentTab); setRootType(currentTab);
}
}, [location]);
function currentMenuItem() {
if (location.pathname.startsWith(`${base}/t/`)) {
return (
<>
<Icon name="hash" />
{location.pathname.split("/").slice(-1)}
</>
);
}
return menuItems.find(a => a.tab === rootType)?.element;
} }
}, [location]);
function currentMenuItem() {
if (location.pathname.startsWith(`${base}/t/`)) {
return (
<>
<Icon name="hash" />
{location.pathname.split("/").slice(-1)}
</>
);
}
return menuItems.find(a => a.tab === rootType)?.element;
}
return ( return (
<div className="root-type"> <div className="root-type">
<Menu <Menu
menuButton={ menuButton={
<button type="button"> <button type="button">
{currentMenuItem()} {currentMenuItem()}
<Icon name="chevronDown" /> <Icon name="chevronDown" />
</button> </button>
} }
align="center" align="center"
menuClassName={() => "ctx-menu"}> menuClassName={() => "ctx-menu"}>
<div className="close-menu-container"> <div className="close-menu-container">
<MenuItem> <MenuItem>
<div className="close-menu" /> <div className="close-menu" />
</MenuItem> </MenuItem>
</div>
{menuItems
.filter(a => a.show)
.map(a => (
<MenuItem
onClick={() => {
navigate(a.path);
}}>
{a.element}
</MenuItem>
))}
{tags.item.map(v => (
<MenuItem
onClick={() => {
navigate(`${base}/t/${v}`);
}}>
<Icon name="hash" />
{v}
</MenuItem>
))}
</Menu>
</div> </div>
); {menuItems
} .filter(a => a.show)
.map(a => (
<MenuItem
onClick={() => {
navigate(a.path);
}}>
{a.element}
</MenuItem>
))}
{tags.item.map(v => (
<MenuItem
onClick={() => {
navigate(`${base}/t/${v}`);
}}>
<Icon name="hash" />
{v}
</MenuItem>
))}
</Menu>
</div>
);
}

View File

@ -53,7 +53,6 @@ export function SpotlightMedia(props: SpotlightMediaProps) {
); );
} }
export function SpotlightMediaModal(props: SpotlightMediaProps) { export function SpotlightMediaModal(props: SpotlightMediaProps) {
return ( return (
<Modal onClose={props.onClose} className="spotlight"> <Modal onClose={props.onClose} className="spotlight">

View File

@ -2,13 +2,7 @@ import "./Thread.css";
import { useMemo, useState, ReactNode, useContext } from "react"; import { useMemo, useState, ReactNode, useContext } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useNavigate, Link, useParams } from "react-router-dom"; import { useNavigate, Link, useParams } from "react-router-dom";
import { import { TaggedNostrEvent, u256, NostrPrefix, EventExt, parseNostrLink } from "@snort/system";
TaggedNostrEvent,
u256,
NostrPrefix,
EventExt,
parseNostrLink,
} from "@snort/system";
import { eventLink, getReactions, getAllReactions } from "SnortUtils"; import { eventLink, getReactions, getAllReactions } from "SnortUtils";
import BackButton from "Element/BackButton"; import BackButton from "Element/BackButton";
@ -160,8 +154,9 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return ( return (
<> <>
<div <div
className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${isLast ? "subthread-last" : "subthread-mid" className={`subthread-container ${hasMultipleNotes ? "subthread-multi" : ""} ${
}`}> isLast ? "subthread-last" : "subthread-mid"
}`}>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
highlight={active === first.id} highlight={active === first.id}
@ -190,8 +185,9 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
return ( return (
<div <div
key={r.id} key={r.id}
className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${lastReply ? "subthread-last" : "subthread-mid" className={`subthread-container ${lastReply ? "" : "subthread-multi"} ${
}`}> lastReply ? "subthread-last" : "subthread-mid"
}`}>
<Divider variant="small" /> <Divider variant="small" />
<Note <Note
className={`thread-note ${lastNote ? "is-last-note" : ""}`} className={`thread-note ${lastNote ? "is-last-note" : ""}`}
@ -213,9 +209,11 @@ export function ThreadRoute() {
const params = useParams(); const params = useParams();
const link = parseNostrLink(params.id ?? "", NostrPrefix.Note); const link = parseNostrLink(params.id ?? "", NostrPrefix.Note);
return <ThreadContextWrapper link={link}> return (
<Thread /> <ThreadContextWrapper link={link}>
</ThreadContextWrapper> <Thread />
</ThreadContextWrapper>
);
} }
export function Thread() { export function Thread() {

View File

@ -113,9 +113,12 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
)} )}
</> </>
)} )}
{mainFeed.map(a => props.noteRenderer?.(a) ?? ( {mainFeed.map(
<Note data={a as TaggedNostrEvent} related={relatedFeed(a.id)} key={a.id} depth={0} /> a =>
))} props.noteRenderer?.(a) ?? (
<Note data={a as TaggedNostrEvent} related={relatedFeed(a.id)} key={a.id} depth={0} />
),
)}
<div className="flex f-center p"> <div className="flex f-center p">
<AsyncButton <AsyncButton
onClick={async () => { onClick={async () => {

View File

@ -4,18 +4,15 @@ import useLogin from "Hooks/useLogin";
import { useMemo } from "react"; import { useMemo } from "react";
export function useArticles() { export function useArticles() {
const {publicKey, follows} = useLogin(); const { publicKey, follows } = useLogin();
const sub = useMemo(() => { const sub = useMemo(() => {
if(!publicKey) return null; if (!publicKey) return null;
const rb = new RequestBuilder(`articles:${publicKey}`); const rb = new RequestBuilder(`articles:${publicKey}`);
rb.withFilter() rb.withFilter().kinds([EventKind.LongFormTextNote]).authors(follows.item).limit(20);
.kinds([EventKind.LongFormTextNote])
.authors(follows.item)
.limit(20);
return rb;
}, [follows.timestamp]);
return useRequestBuilder(NoteCollection, sub); return rb;
} }, [follows.timestamp]);
return useRequestBuilder(NoteCollection, sub);
}

View File

@ -3,20 +3,20 @@ import { useEffect } from "react";
import useLogin from "./useLogin"; import useLogin from "./useLogin";
export function useLoginRelays() { export function useLoginRelays() {
const { relays } = useLogin(); const { relays } = useLogin();
useEffect(() => { useEffect(() => {
if (relays) { if (relays) {
(async () => { (async () => {
for (const [k, v] of Object.entries(relays.item)) { for (const [k, v] of Object.entries(relays.item)) {
await System.ConnectToRelay(k, v); await System.ConnectToRelay(k, v);
}
for (const v of System.Sockets) {
if (!relays.item[v.address] && !v.ephemeral) {
System.DisconnectRelay(v.address);
}
}
})();
} }
}, [relays]); for (const v of System.Sockets) {
} if (!relays.item[v.address] && !v.ephemeral) {
System.DisconnectRelay(v.address);
}
}
})();
}
}, [relays]);
}

View File

@ -3,13 +3,13 @@ import { ParsedFragment, transformText } from "@snort/system";
const TextCache = new Map<string, Array<ParsedFragment>>(); const TextCache = new Map<string, Array<ParsedFragment>>();
export function transformTextCached(id: string, content: string, tags: Array<Array<string>>) { export function transformTextCached(id: string, content: string, tags: Array<Array<string>>) {
const cached = TextCache.get(id); const cached = TextCache.get(id);
if (cached) return cached; if (cached) return cached;
const newCache = transformText(content, tags); const newCache = transformText(content, tags);
TextCache.set(id, newCache); TextCache.set(id, newCache);
return newCache; return newCache;
} }
export function useTextTransformer(id: string, content: string, tags: Array<Array<string>>) { export function useTextTransformer(id: string, content: string, tags: Array<Array<string>>) {
return transformTextCached(id, content, tags); return transformTextCached(id, content, tags);
} }

View File

@ -2,31 +2,30 @@ import { useEffect } from "react";
import useLogin from "./useLogin"; import useLogin from "./useLogin";
export function useTheme() { export function useTheme() {
const { preferences } = useLogin(); const { preferences } = useLogin();
function setTheme(theme: "light" | "dark") { function setTheme(theme: "light" | "dark") {
const elm = document.documentElement; const elm = document.documentElement;
if (theme === "light" && !elm.classList.contains("light")) { if (theme === "light" && !elm.classList.contains("light")) {
elm.classList.add("light"); elm.classList.add("light");
} else if (theme === "dark" && elm.classList.contains("light")) { } else if (theme === "dark" && elm.classList.contains("light")) {
elm.classList.remove("light"); elm.classList.remove("light");
}
} }
}
useEffect(() => { useEffect(() => {
const osTheme = window.matchMedia("(prefers-color-scheme: light)"); const osTheme = window.matchMedia("(prefers-color-scheme: light)");
setTheme( setTheme(
preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark" preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark",
); );
osTheme.onchange = e => { osTheme.onchange = e => {
if (preferences.theme === "system") { if (preferences.theme === "system") {
setTheme(e.matches ? "light" : "dark"); setTheme(e.matches ? "light" : "dark");
} }
}; };
return () => { return () => {
osTheme.onchange = null; osTheme.onchange = null;
}; };
}, [preferences.theme]); }, [preferences.theme]);
}
}

View File

@ -1,110 +1,116 @@
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { EventExt, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, u256, Thread as ThreadInfo, } from "@snort/system"; import {
EventExt,
EventKind,
NostrLink,
NostrPrefix,
TaggedNostrEvent,
u256,
Thread as ThreadInfo,
} from "@snort/system";
import useThreadFeed from "Feed/ThreadFeed"; import useThreadFeed from "Feed/ThreadFeed";
import { findTag } from "SnortUtils"; import { findTag } from "SnortUtils";
import { ReactNode, createContext, useMemo, useState } from "react"; import { ReactNode, createContext, useMemo, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
export interface ThreadContext { export interface ThreadContext {
current: string, current: string;
root?: TaggedNostrEvent, root?: TaggedNostrEvent;
chains: Map<string, Array<TaggedNostrEvent>>, chains: Map<string, Array<TaggedNostrEvent>>;
data: Array<TaggedNostrEvent>, data: Array<TaggedNostrEvent>;
setCurrent: (i: string) => void; setCurrent: (i: string) => void;
} }
export const ThreadContext = createContext({} as ThreadContext) export const ThreadContext = createContext({} as 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(link.id); const [currentId, setCurrentId] = useState(link.id);
const thread = useThreadFeed(link); const thread = useThreadFeed(link);
const chains = useMemo(() => { const chains = useMemo(() => {
const chains = new Map<u256, Array<TaggedNostrEvent>>(); const chains = new Map<u256, Array<TaggedNostrEvent>>();
if (thread.data) { if (thread.data) {
thread.data thread.data
?.filter(a => a.kind === EventKind.TextNote) ?.filter(a => a.kind === EventKind.TextNote)
.sort((a, b) => b.created_at - a.created_at) .sort((a, b) => b.created_at - a.created_at)
.forEach(v => { .forEach(v => {
const t = EventExt.extractThread(v); const t = EventExt.extractThread(v);
let replyTo = t?.replyTo?.value ?? t?.root?.value; let replyTo = t?.replyTo?.value ?? t?.root?.value;
if (t?.root?.key === "a" && t?.root?.value) { if (t?.root?.key === "a" && t?.root?.value) {
const parsed = t.root.value.split(":"); const parsed = t.root.value.split(":");
replyTo = thread.data?.find( replyTo = thread.data?.find(
a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2] a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2],
)?.id; )?.id;
} }
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;
}, [thread.data]);
// 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 currentNote =
thread.data?.find(
ne =>
ne.id === currentId ||
(link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author),
) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
if (currentNote) {
const currentThread = EventExt.extractThread(currentNote);
const isRoot = (ne?: ThreadInfo) => ne === undefined;
if (isRoot(currentThread)) {
return currentNote;
}
const replyTo = currentThread?.replyTo ?? currentThread?.root;
// sometimes the root event ID is missing, and we can only take the happy path if the root event ID exists
if (replyTo) {
if (replyTo.key === "a" && replyTo.value) {
const parsed = replyTo.value.split(":");
return thread.data?.find(
a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2],
);
} }
return chains; if (replyTo.value) {
}, [thread.data]); return thread.data?.find(a => a.id === replyTo.value);
// 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 currentNote =
thread.data?.find(
ne =>
ne.id === currentId ||
(link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author)
) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
if (currentNote) {
const currentThread = EventExt.extractThread(currentNote);
const isRoot = (ne?: ThreadInfo) => ne === undefined;
if (isRoot(currentThread)) {
return currentNote;
}
const replyTo = currentThread?.replyTo ?? currentThread?.root;
// sometimes the root event ID is missing, and we can only take the happy path if the root event ID exists
if (replyTo) {
if (replyTo.key === "a" && replyTo.value) {
const parsed = replyTo.value.split(":");
return thread.data?.find(
a => a.kind === Number(parsed[0]) && a.pubkey === parsed[1] && findTag(a, "d") === parsed[2]
);
}
if (replyTo.value) {
return thread.data?.find(a => a.id === replyTo.value);
}
}
const possibleRoots = thread.data?.filter(a => {
const thread = EventExt.extractThread(a);
return isRoot(thread);
});
if (possibleRoots) {
// worst case we need to check every possible root to see which one contains the current note as a child
for (const ne of possibleRoots) {
const children = chains.get(ne.id) ?? [];
if (children.find(ne => ne.id === currentId)) {
return ne;
}
}
}
} }
}, [thread.data, currentId, location]); }
const ctxValue = useMemo(() => { const possibleRoots = thread.data?.filter(a => {
return { const thread = EventExt.extractThread(a);
current: currentId, return isRoot(thread);
root, });
chains, if (possibleRoots) {
data: thread.data, // worst case we need to check every possible root to see which one contains the current note as a child
setCurrent: v => setCurrentId(v) for (const ne of possibleRoots) {
} as ThreadContext const children = chains.get(ne.id) ?? [];
}, [root, chains]);
return <ThreadContext.Provider value={ctxValue}> if (children.find(ne => ne.id === currentId)) {
{children} return ne;
</ThreadContext.Provider> }
} }
}
}
}, [thread.data, currentId, location]);
const ctxValue = useMemo(() => {
return {
current: currentId,
root,
chains,
data: thread.data,
setCurrent: v => setCurrentId(v),
} as ThreadContext;
}, [root, chains]);
return <ThreadContext.Provider value={ctxValue}>{children}</ThreadContext.Provider>;
}

View File

@ -1,97 +1,97 @@
.deck-layout { .deck-layout {
display: flex; display: flex;
height: 100vh; height: 100vh;
overflow-y: hidden; overflow-y: hidden;
} }
.deck-layout .deck-cols { .deck-layout .deck-cols {
display: flex; display: flex;
height: 100vh; height: 100vh;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
} }
.deck-layout .deck-cols .deck-col-header { .deck-layout .deck-cols .deck-col-header {
padding: 8px 16px; padding: 8px 16px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-collapse: collapse; border-collapse: collapse;
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
min-height: 40px; min-height: 40px;
max-height: 40px; max-height: 40px;
} }
.deck-layout .deck-cols .deck-col-header:not(:last-of-type) { .deck-layout .deck-cols .deck-col-header:not(:last-of-type) {
border-right: 0; border-right: 0;
} }
.deck-layout .deck-cols > div { .deck-layout .deck-cols > div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
width: 550px; width: 550px;
min-width: 550px; min-width: 550px;
} }
.deck-layout .deck-cols > div > div:not(:first-of-type) { .deck-layout .deck-cols > div > div:not(:first-of-type) {
overflow-y: scroll; overflow-y: scroll;
} }
.image-grid { .image-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 4px; gap: 4px;
} }
.image-grid > .media-note { .image-grid > .media-note {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-image: var(--img); background-image: var(--img);
background-position: center; background-position: center;
background-size: cover; background-size: cover;
aspect-ratio: 1; aspect-ratio: 1;
cursor: pointer; cursor: pointer;
} }
.thread-overlay .modal-body { .thread-overlay .modal-body {
background-color: unset; background-color: unset;
padding: 0; padding: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-radius: unset; border-radius: unset;
gap: 16px; gap: 16px;
--border-color: #3A3A3A; --border-color: #3a3a3a;
} }
.thread-overlay .modal-body > div:last-of-type { .thread-overlay .modal-body > div:last-of-type {
width: 550px; width: 550px;
min-width: 550px; min-width: 550px;
height: 100vh; height: 100vh;
overflow-y: auto; overflow-y: auto;
background-color: var(--gray-superdark); background-color: var(--gray-superdark);
} }
.thread-overlay .spotlight { .thread-overlay .spotlight {
flex-grow: 1; flex-grow: 1;
margin: auto; margin: auto;
text-align: center; text-align: center;
} }
.thread-overlay .spotlight .details { .thread-overlay .spotlight .details {
right: calc(28px + 550px + 16px); right: calc(28px + 550px + 16px);
} }
.thread-overlay .spotlight .right { .thread-overlay .spotlight .right {
right: calc(24px + 550px + 16px); right: calc(24px + 550px + 16px);
} }
.thread-overlay .spotlight img, .thread-overlay .spotlight img,
.thread-overlay .spotlight video { .thread-overlay .spotlight video {
max-width: calc(100vw - 550px - 16px); max-width: calc(100vw - 550px - 16px);
} }
.thread-overlay .main-content { .thread-overlay .main-content {
border: 0; border: 0;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }

View File

@ -25,127 +25,147 @@ import useLogin from "Hooks/useLogin";
type Cols = "notes" | "articles" | "media" | "streams" | "notifications"; type Cols = "notes" | "articles" | "media" | "streams" | "notifications";
export function SnortDeckLayout() { export function SnortDeckLayout() {
const login = useLogin(); const login = useLogin();
const navigate = useNavigate(); const navigate = useNavigate();
const [thread, setThread] = useState<string>(); const [thread, setThread] = useState<string>();
useLoginFeed(); useLoginFeed();
useTheme(); useTheme();
useLoginRelays(); useLoginRelays();
useEffect(() => { useEffect(() => {
if (!login.publicKey) { if (!login.publicKey) {
navigate("/"); navigate("/");
} }
}, [login]); }, [login]);
if (!login.publicKey) return null; if (!login.publicKey) return null;
const cols = ["notes", "media", "notifications", "articles"] as Array<Cols>; const cols = ["notes", "media", "notifications", "articles"] as Array<Cols>;
return <div className="deck-layout"> return (
<DeckNav /> <div className="deck-layout">
<div className="deck-cols"> <DeckNav />
{cols.map(c => { <div className="deck-cols">
switch (c) { {cols.map(c => {
case "notes": return <NotesCol /> switch (c) {
case "media": return <MediaCol setThread={setThread} /> case "notes":
case "articles": return <ArticlesCol /> return <NotesCol />;
case "notifications": return <NotificationsCol /> case "media":
} return <MediaCol setThread={setThread} />;
})} case "articles":
</div> return <ArticlesCol />;
{thread && <> case "notifications":
<Modal onClose={() => setThread(undefined)} className="thread-overlay"> return <NotificationsCol />;
<ThreadContextWrapper link={createNostrLink(NostrPrefix.Note, thread)}> }
<SpotlightFromThread onClose={() => setThread(undefined)} /> })}
<div> </div>
<Thread /> {thread && (
</div> <>
</ThreadContextWrapper> <Modal onClose={() => setThread(undefined)} className="thread-overlay">
</Modal> <ThreadContextWrapper link={createNostrLink(NostrPrefix.Note, thread)}>
</>} <SpotlightFromThread onClose={() => setThread(undefined)} />
<Toaster /> <div>
<Thread />
</div>
</ThreadContextWrapper>
</Modal>
</>
)}
<Toaster />
</div> </div>
);
} }
function SpotlightFromThread({ onClose }: { onClose: () => void }) { function SpotlightFromThread({ onClose }: { onClose: () => void }) {
const thread = useContext(ThreadContext); const thread = useContext(ThreadContext);
const parsed = thread.root ? transformTextCached(thread.root.id, thread.root.content, thread.root.tags) : []; const parsed = thread.root ? transformTextCached(thread.root.id, thread.root.content, thread.root.tags) : [];
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/")); const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
return <SpotlightMedia images={images.map(a => a.content)} idx={0} onClose={onClose} /> return <SpotlightMedia images={images.map(a => a.content)} idx={0} onClose={onClose} />;
} }
function NotesCol() { function NotesCol() {
return ( return (
<div> <div>
<div className="deck-col-header flex"> <div className="deck-col-header flex">
<div className="flex f-1 g8"> <div className="flex f-1 g8">
<Icon name="rows-01" size={24} /> <Icon name="rows-01" size={24} />
<FormattedMessage defaultMessage="Notes" /> <FormattedMessage defaultMessage="Notes" />
</div>
<div className="f-1">
<RootTabs base="/deck" />
</div>
</div>
<div>
<Outlet />
</div>
</div> </div>
); <div className="f-1">
<RootTabs base="/deck" />
</div>
</div>
<div>
<Outlet />
</div>
</div>
);
} }
function ArticlesCol() { function ArticlesCol() {
return ( return (
<div> <div>
<div className="deck-col-header flex g8"> <div className="deck-col-header flex g8">
<Icon name="file-06" size={24} /> <Icon name="file-06" size={24} />
<FormattedMessage defaultMessage="Articles" /> <FormattedMessage defaultMessage="Articles" />
</div> </div>
<div> <div>
<Articles /> <Articles />
</div> </div>
</div> </div>
); );
} }
function MediaCol({ setThread }: { setThread: (e: string) => void }) { function MediaCol({ setThread }: { setThread: (e: string) => void }) {
const { proxy } = useImgProxy(); const { proxy } = useImgProxy();
return ( return (
<div> <div>
<div className="deck-col-header flex g8"> <div className="deck-col-header flex g8">
<Icon name="camera-lens" size={24} /> <Icon name="camera-lens" size={24} />
<FormattedMessage defaultMessage="Media" /> <FormattedMessage defaultMessage="Media" />
</div> </div>
<div className="image-grid p"> <div className="image-grid p">
<TimelineFollows postsOnly={true} liveStreams={false} noteFilter={e => { <TimelineFollows
const parsed = transformTextCached(e.id, e.content, e.tags); postsOnly={true}
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/")); liveStreams={false}
return images.length > 0; noteFilter={e => {
}} noteRenderer={e => { const parsed = transformTextCached(e.id, e.content, e.tags);
const parsed = transformTextCached(e.id, e.content, e.tags); const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/")); return images.length > 0;
}}
noteRenderer={e => {
const parsed = transformTextCached(e.id, e.content, e.tags);
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
return <div className="media-note" key={e.id} style={{ return (
"--img": `url(${proxy(images[0].content)})` <div
} as CSSProperties} onClick={() => setThread(e.id)}></div> className="media-note"
}} /> key={e.id}
</div> style={
</div> {
); "--img": `url(${proxy(images[0].content)})`,
} as CSSProperties
}
onClick={() => setThread(e.id)}></div>
);
}}
/>
</div>
</div>
);
} }
function NotificationsCol() { function NotificationsCol() {
return (
return ( <div>
<div> <div className="deck-col-header flex g8">
<div className="deck-col-header flex g8"> <Icon name="bell-02" size={24} />
<Icon name="bell-02" size={24} /> <FormattedMessage defaultMessage="Notifications" />
<FormattedMessage defaultMessage="Notifications" /> </div>
</div> <div>
<div> <NotificationsPage />
<NotificationsPage /> </div>
</div> </div>
</div> );
); }
}

View File

@ -164,12 +164,12 @@ const DefaultTab = () => {
const tab = publicKey ? preferences.defaultRootTab ?? `notes` : `trending/notes`; const tab = publicKey ? preferences.defaultRootTab ?? `notes` : `trending/notes`;
const elm = RootTabRoutes.find(a => a.path === tab)?.element; const elm = RootTabRoutes.find(a => a.path === tab)?.element;
return elm; return elm;
} };
export const RootTabRoutes = [ export const RootTabRoutes = [
{ {
path: "", path: "",
element: <DefaultTab /> element: <DefaultTab />,
}, },
{ {
path: "global", path: "global",

View File

@ -262,7 +262,7 @@ button.icon:hover {
} }
.light .btn { .light .btn {
color: #64748B; color: #64748b;
} }
.btn-warn { .btn-warn {

View File

@ -211,8 +211,8 @@ export const router = createBrowserRouter([
} }
return null; return null;
}, },
children: RootTabRoutes children: RootTabRoutes,
} },
]); ]);
const root = ReactDOM.createRoot(unwrap(document.getElementById("root"))); const root = ReactDOM.createRoot(unwrap(document.getElementById("root")));

View File

@ -10,8 +10,8 @@
"/4tOwT": "Überspringen", "/4tOwT": "Überspringen",
"/JE/X+": "Konto Hilfe", "/JE/X+": "Konto Hilfe",
"/PCavi": "Öffentlich", "/PCavi": "Öffentlich",
"/RD0e2": "Nostr nutzt digitale Signaturen, um manipulationssichere Notes zu erstellen, welche sicher auf viele Relays repliziert werden können, um eine redundante Speicherung deiner Inhalte zu bieten.", "/RD0e2": "Nostr nutzt digitale Signaturen, um manipulationssichere Notes zu erstellen, welche sicher auf viele Relais repliziert werden können, um eine redundante Speicherung deiner Inhalte zu bieten.",
"/Xf4UW": "Send anonymous usage metrics", "/Xf4UW": "Anonyme Nutzungsmetriken senden",
"/d6vEc": "Mach dein Profil leichter zu finden und zu teilen", "/d6vEc": "Mach dein Profil leichter zu finden und zu teilen",
"/n5KSF": "{n} ms", "/n5KSF": "{n} ms",
"00LcfG": "Mehr laden", "00LcfG": "Mehr laden",
@ -52,7 +52,7 @@
"4Z3t5i": "Verwende imgproxy um Bilder zu komprimieren", "4Z3t5i": "Verwende imgproxy um Bilder zu komprimieren",
"4rYCjn": "Notiz an mich selbst", "4rYCjn": "Notiz an mich selbst",
"5BVs2e": "Zap", "5BVs2e": "Zap",
"5CB6zB": "Zap Splits", "5CB6zB": "Zap-Aufteilungen",
"5JcXdV": "Konto erstellen", "5JcXdV": "Konto erstellen",
"5oTnfy": "Handle kaufen", "5oTnfy": "Handle kaufen",
"5rOdPG": "Sobald du deine Erweiterung für die Schlüsselverwaltung eingerichtet und einen Schlüssel generiert hast, kannst du unserem Prozess für neue Benutzer folgen, um dein Profil einzurichten und einige interessante Leute auf Nostr zu finden, denen du folgen kannst.", "5rOdPG": "Sobald du deine Erweiterung für die Schlüsselverwaltung eingerichtet und einen Schlüssel generiert hast, kannst du unserem Prozess für neue Benutzer folgen, um dein Profil einzurichten und einige interessante Leute auf Nostr zu finden, denen du folgen kannst.",
@ -61,7 +61,7 @@
"5ykRmX": "Zap senden", "5ykRmX": "Zap senden",
"65BmHb": "Bild von {host} konnte nicht durch Proxy geladen werden, klicke hier, um es direkt zu laden", "65BmHb": "Bild von {host} konnte nicht durch Proxy geladen werden, klicke hier, um es direkt zu laden",
"6Yfvvp": "Bekomme eine Identifikation", "6Yfvvp": "Bekomme eine Identifikation",
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured", "6bgpn+": "Nicht alle Clients unterstützen dies, deshalb kann es sein, dass du immer noch einige Zaps erhältst, als ob Zap-Aufteilungen nicht konfiguriert wäre",
"6ewQqw": "Gefällt ({n})", "6ewQqw": "Gefällt ({n})",
"6uMqL1": "Nicht bezahlt", "6uMqL1": "Nicht bezahlt",
"7+Domh": "Notes", "7+Domh": "Notes",
@ -69,8 +69,8 @@
"7hp70g": "NIP-05", "7hp70g": "NIP-05",
"8/vBbP": "Reposts ({n})", "8/vBbP": "Reposts ({n})",
"89q5wc": "Reposts bestätigen", "89q5wc": "Reposts bestätigen",
"8QDesP": "Zap {n} sats", "8QDesP": "{n} sats zappen",
"8Y6bZQ": "Invalid zap split: {input}", "8Y6bZQ": "Ungültige Zap-Aufteilung: {input}",
"8g2vyB": "Name ist zu lang", "8g2vyB": "Name ist zu lang",
"8v1NN+": "Verbindungsphrase", "8v1NN+": "Verbindungsphrase",
"9+Ddtu": "Weiter", "9+Ddtu": "Weiter",
@ -81,7 +81,7 @@
"9pMqYs": "Nostr-Adresse", "9pMqYs": "Nostr-Adresse",
"9wO4wJ": "Lightning Zahlungsanforderung", "9wO4wJ": "Lightning Zahlungsanforderung",
"ADmfQT": "Vorherige", "ADmfQT": "Vorherige",
"AGNz71": "Zap {n} sats an Alle", "AGNz71": "Allen {n} sats zappen",
"ASRK0S": "Dieser Autor wurde stummgeschalten", "ASRK0S": "Dieser Autor wurde stummgeschalten",
"Adk34V": "Profil erstellen", "Adk34V": "Profil erstellen",
"Ai8VHU": "Unbegrenzte Note Speicherung auf Snort Relais", "Ai8VHU": "Unbegrenzte Note Speicherung auf Snort Relais",
@ -91,7 +91,7 @@
"B4C47Y": "Name ist zu kurz", "B4C47Y": "Name ist zu kurz",
"B6+XJy": "hat gezappt", "B6+XJy": "hat gezappt",
"B6H7eJ": "nsec, npub, nip-05, hex", "B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Schreibe Zugriff auf Snort-Relais mit 1 Jahr Event Speicherung", "BGCM48": "Schreibzugriff auf Snort Relais, mit 1 Jahr Speicherung von Events",
"BOUMjw": "Keine Nostr-Benutzer gefunden für {twitterUsername}", "BOUMjw": "Keine Nostr-Benutzer gefunden für {twitterUsername}",
"BOr9z/": "Snort ist ein Open-Source-Projekt, von begeisterten in ihrer Freizeit entwickelt", "BOr9z/": "Snort ist ein Open-Source-Projekt, von begeisterten in ihrer Freizeit entwickelt",
"BWpuKl": "Aktualisieren", "BWpuKl": "Aktualisieren",
@ -116,7 +116,7 @@
"EPYwm7": "Dein privater Schlüssel ist dein Passwort. Wenn du diesen Schlüssel verlierst, hast du keinen Zugang mehr zu deinem Konto! Kopiere ihn und bewahre ihn an einem sicheren Ort auf. Es gibt keine Möglichkeit, deinen privaten Schlüssel wiederherzustellen.", "EPYwm7": "Dein privater Schlüssel ist dein Passwort. Wenn du diesen Schlüssel verlierst, hast du keinen Zugang mehr zu deinem Konto! Kopiere ihn und bewahre ihn an einem sicheren Ort auf. Es gibt keine Möglichkeit, deinen privaten Schlüssel wiederherzustellen.",
"EWyQH5": "Global", "EWyQH5": "Global",
"Ebl/B2": "Auf {lang} Übersetzen", "Ebl/B2": "Auf {lang} Übersetzen",
"EcZF24": "Benutzerdefinierte Relays", "EcZF24": "Benutzerdefinierte Relais",
"EcglP9": "Schlüssel", "EcglP9": "Schlüssel",
"EnCOBJ": "Kaufen", "EnCOBJ": "Kaufen",
"Eqjl5K": "Nur Snort und unsere integrierten Partner bieten farbige Domainnamen als NIP-05 Kennzeichen. Sie können aber auch gerne andere Dienste nutzen.", "Eqjl5K": "Nur Snort und unsere integrierten Partner bieten farbige Domainnamen als NIP-05 Kennzeichen. Sie können aber auch gerne andere Dienste nutzen.",
@ -145,7 +145,7 @@
"HOzFdo": "Stummgeschaltet", "HOzFdo": "Stummgeschaltet",
"HWbkEK": "Cache leeren und neu laden", "HWbkEK": "Cache leeren und neu laden",
"HbefNb": "Wallet öffnen", "HbefNb": "Wallet öffnen",
"I9zn6f": "Pubkey", "I9zn6f": "Öfftl. Schlüssel",
"IDjHJ6": "Danke für die Verwendung von Snort. Wir würden uns über eine Spende freuen.", "IDjHJ6": "Danke für die Verwendung von Snort. Wir würden uns über eine Spende freuen.",
"IEwZvs": "Sind sie sicher, dass sie diese Notiz entpinnen möchten?", "IEwZvs": "Sind sie sicher, dass sie diese Notiz entpinnen möchten?",
"IKKHqV": "Folgt", "IKKHqV": "Folgt",
@ -155,7 +155,7 @@
"Ix8l+B": "Angesagte Notes", "Ix8l+B": "Angesagte Notes",
"J+dIsA": "Abonnements", "J+dIsA": "Abonnements",
"JCIgkj": "Benutzername", "JCIgkj": "Benutzername",
"JGrt9q": "Send sats to {name}", "JGrt9q": "Sats an {name} senden",
"JHEHCk": "Zaps ({n})", "JHEHCk": "Zaps ({n})",
"JPFYIM": "Keine Lightning-Adresse", "JPFYIM": "Keine Lightning-Adresse",
"JeoS4y": "Repost", "JeoS4y": "Repost",
@ -175,7 +175,7 @@
"LgbKvU": "Kommentar", "LgbKvU": "Kommentar",
"Lu5/Bj": "In Zapstr öffnen", "Lu5/Bj": "In Zapstr öffnen",
"Lw+I+J": "{n,plural,=0{{name} zappte} other{{name} & {n} andere zappten}}", "Lw+I+J": "{n,plural,=0{{name} zappte} other{{name} & {n} andere zappten}}",
"LwYmVi": "Zaps on this note will be split to the following users.", "LwYmVi": "Zaps auf diese Note werden an die folgenden Nutzer aufgeteilt.",
"M3Oirc": "Debug Menüs", "M3Oirc": "Debug Menüs",
"MBAYRO": "Zeigt \"ID kopieren\" und \"Event JSON kopieren\" im Kontextmenu jeder Nachricht an", "MBAYRO": "Zeigt \"ID kopieren\" und \"Event JSON kopieren\" im Kontextmenu jeder Nachricht an",
"MI2jkA": "Nicht verfügbar:", "MI2jkA": "Nicht verfügbar:",
@ -221,7 +221,7 @@
"RoOyAh": "Relais", "RoOyAh": "Relais",
"Rs4kCE": "Lesezeichen", "Rs4kCE": "Lesezeichen",
"RwFaYs": "Sortieren", "RwFaYs": "Sortieren",
"SMO+on": "Send zap to {name}", "SMO+on": "Zap an {name} senden",
"SOqbe9": "Lightning-Adresse aktualisieren", "SOqbe9": "Lightning-Adresse aktualisieren",
"SP0+yi": "Abonnement kaufen", "SP0+yi": "Abonnement kaufen",
"SX58hM": "Kopieren", "SX58hM": "Kopieren",
@ -251,10 +251,10 @@
"W2PiAr": "{n} Blockiert", "W2PiAr": "{n} Blockiert",
"W9355R": "Stummschaltung aufheben", "W9355R": "Stummschaltung aufheben",
"WONP5O": "Finden Sie Ihre Twitter Kontakte auf Nostr (Daten zur Verfügung gestellt von {provider})", "WONP5O": "Finden Sie Ihre Twitter Kontakte auf Nostr (Daten zur Verfügung gestellt von {provider})",
"WvGmZT": "npub / nprofile / nostr address", "WvGmZT": "npub / nprofile / Nostr-Adresse",
"WxthCV": "z.B. Jack", "WxthCV": "z.B. Jack",
"X7xU8J": "nsec, npub, nip-05, hex, mnemonic", "X7xU8J": "nsec, npub, nip-05, hex, mnemonic",
"XECMfW": "Send usage metrics", "XECMfW": "Nutzungsmetriken senden",
"XICsE8": "Datei-Hosts", "XICsE8": "Datei-Hosts",
"XgWvGA": "Reaktionen", "XgWvGA": "Reaktionen",
"Xopqkl": "Dein standardmäßiger Zap-Betrag ist {number} sats, Beispielwerte werden daraus berechnet.", "Xopqkl": "Dein standardmäßiger Zap-Betrag ist {number} sats, Beispielwerte werden daraus berechnet.",
@ -263,7 +263,7 @@
"Y31HTH": "Unterstütze die Entwicklung von Snort", "Y31HTH": "Unterstütze die Entwicklung von Snort",
"YDURw6": "URL des Dienstes", "YDURw6": "URL des Dienstes",
"YXA3AH": "Reaktionen aktivieren", "YXA3AH": "Reaktionen aktivieren",
"Z0FDj+": "Abonniere Snort {plan} für {price} und erhalte folgende Belohnungen", "Z0FDj+": "Abonniere Snort {plan} für {price} und erhalte folgende Prämien",
"Z4BMCZ": "Verbindungs-Passphrase eingeben", "Z4BMCZ": "Verbindungs-Passphrase eingeben",
"ZKORll": "Jetzt aktivieren", "ZKORll": "Jetzt aktivieren",
"ZLmyG9": "Mitwirkende", "ZLmyG9": "Mitwirkende",
@ -340,11 +340,11 @@
"jvo0vs": "Speichern", "jvo0vs": "Speichern",
"jzgQ2z": "{n} Reaktionen", "jzgQ2z": "{n} Reaktionen",
"k2veDA": "Schreiben", "k2veDA": "Schreiben",
"k7sKNy": "Unser eigener NIP-05-Verifizierungsdienst unterstützt die Entwicklung dieser Website. Unterstütze uns und erhalte ein Sonderabzeichen für deine Webseite!", "k7sKNy": "Unser eigener NIP-05-Verifizierungsdienst unterstützt die Entwicklung dieser Website. Unterstütze uns und erhalte ein Abzeichen auf unserer Seite!",
"kJYo0u": "{n,plural,=0{{name} hat gerepostet} other{{name} & {n} andere haben gerepostet}}", "kJYo0u": "{n,plural,=0{{name} hat gerepostet} other{{name} & {n} andere haben gerepostet}}",
"kaaf1E": "jetzt", "kaaf1E": "jetzt",
"kuPHYE": "{n,plural,=0{{name} gefällt das} other{{name} & {n} anderen gefällt das}}", "kuPHYE": "{n,plural,=0{{name} gefällt das} other{{name} & {n} anderen gefällt das}}",
"l+ikU1": "Alles im {plan}", "l+ikU1": "Alles aus {plan}",
"lBboHo": "Wenn du ein paar andere ausprobieren möchtest, besuche {link} für mehr!", "lBboHo": "Wenn du ein paar andere ausprobieren möchtest, besuche {link} für mehr!",
"lCILNz": "Jetzt kaufen", "lCILNz": "Jetzt kaufen",
"lD3+8a": "Bezahlen", "lD3+8a": "Bezahlen",
@ -380,7 +380,7 @@
"oxCa4R": "Das Erhalten eines Identifikators hilft Menschen, die Sie kennen, Ihre Identität zu bestätigen. Viele Leute können einen Benutzernamen wie @jack haben, aber es kann nur einen jack@cash.app geben.", "oxCa4R": "Das Erhalten eines Identifikators hilft Menschen, die Sie kennen, Ihre Identität zu bestätigen. Viele Leute können einen Benutzernamen wie @jack haben, aber es kann nur einen jack@cash.app geben.",
"p4N05H": "Hochladen", "p4N05H": "Hochladen",
"p85Uwy": "Aktive Abonnements", "p85Uwy": "Aktive Abonnements",
"pI+77w": "Herunterladbare Backups von Snort Relais", "pI+77w": "Herunterladbare Backups vom Snort Relais",
"puLNUJ": "Anheften", "puLNUJ": "Anheften",
"pzTOmv": "Follower", "pzTOmv": "Follower",
"qD9EUF": "E-Mail <> DM Brücke für deine Snort Nostr-Adresse", "qD9EUF": "E-Mail <> DM Brücke für deine Snort Nostr-Adresse",
@ -401,19 +401,19 @@
"rudscU": "Abonnements konnten nicht geladen werden, bitte versuchen Sie es später erneut", "rudscU": "Abonnements konnten nicht geladen werden, bitte versuchen Sie es später erneut",
"sUNhQE": "Nutzer", "sUNhQE": "Nutzer",
"sWnYKw": "Snort wurde darauf konzipiert ein Twitter ähnliches Erlebnis zu bieten.", "sWnYKw": "Snort wurde darauf konzipiert ein Twitter ähnliches Erlebnis zu bieten.",
"sZQzjQ": "Failed to parse zap split: {input}", "sZQzjQ": "Fehler beim Parsen der Zap-Aufteilung: {input}",
"svOoEH": "Namens-Squatting und Impersonation sind nicht erlaubt. Snort und unsere Partner behalten sich das Recht vor, bei einem Verstoß gegen diese Regel dein Handle zu löschen (nicht dein Konto - das kann dir niemand wegnehmen).", "svOoEH": "Namens-Squatting und Impersonation sind nicht erlaubt. Snort und unsere Partner behalten sich das Recht vor, bei einem Verstoß gegen diese Regel dein Handle zu löschen (nicht dein Konto - das kann dir niemand wegnehmen).",
"tOdNiY": "Dunkel", "tOdNiY": "Dunkel",
"th5lxp": "Sende Note zu einer Untergruppe deiner Schreib-Relays", "th5lxp": "Sende Note zu einer Untergruppe deiner Schreib-Relays",
"thnRpU": "Eine NIP-05 Verifizierung kann helfen:", "thnRpU": "Eine NIP-05 Verifizierung kann helfen:",
"ttxS0b": "Unterstützer Abzeichen", "ttxS0b": "Unterstützer-Abzeichen",
"u/vOPu": "Bezahlt", "u/vOPu": "Bezahlt",
"u4bHcR": "Sieh dir hier den Code an: {link}", "u4bHcR": "Sieh dir hier den Code an: {link}",
"uSV4Ti": "Reposts müssen manuell bestätigt werden", "uSV4Ti": "Reposts müssen manuell bestätigt werden",
"usAvMr": "Profil anpassen", "usAvMr": "Profil anpassen",
"ut+2Cd": "Holen Sie sich einen Partner-Identifikator", "ut+2Cd": "Holen Sie sich einen Partner-Identifikator",
"v8lolG": "Chat starten", "v8lolG": "Chat starten",
"vOKedj": "{n,plural,one {}=1{& {n} other} other{& {n} andere}}", "vOKedj": "{n,plural,one {}=1{& {n} anderer} other{& {n} andere}}",
"vU71Ez": "Bezahlen mit {wallet}", "vU71Ez": "Bezahlen mit {wallet}",
"vZ4quW": "NIP-05 ist eine DNS-basierte Verifizierungsspezifikation, die dabei hilft, dich als echten Benutzer zu validieren.", "vZ4quW": "NIP-05 ist eine DNS-basierte Verifizierungsspezifikation, die dabei hilft, dich als echten Benutzer zu validieren.",
"vhlWFg": "Umfrageoptionen", "vhlWFg": "Umfrageoptionen",
@ -442,7 +442,7 @@
"y1Z3or": "Sprache", "y1Z3or": "Sprache",
"yCLnBC": "LNURL oder Lightning-Adresse", "yCLnBC": "LNURL oder Lightning-Adresse",
"yCmnnm": "Global lesen von", "yCmnnm": "Global lesen von",
"zCb8fX": "Weight", "zCb8fX": "Gewichtung",
"zFegDD": "Kontakt", "zFegDD": "Kontakt",
"zINlao": "Eigentümer", "zINlao": "Eigentümer",
"zQvVDJ": "Alle", "zQvVDJ": "Alle",

View File

@ -23,7 +23,7 @@ export function linkToEventTag(link: NostrLink) {
} }
export function tagToNostrLink(tag: Array<string>) { export function tagToNostrLink(tag: Array<string>) {
switch(tag[0]) { switch (tag[0]) {
case "e": { case "e": {
return createNostrLink(NostrPrefix.Event, tag[1], tag.slice(2)); return createNostrLink(NostrPrefix.Event, tag[1], tag.slice(2));
} }