Merge remote-tracking branch 'kieran/main'

This commit is contained in:
Martti Malmi
2023-11-26 15:52:50 +02:00
69 changed files with 761 additions and 437 deletions

View File

@ -1,3 +1,55 @@
# v0.1.23
## Added
- DeepL translate api (Automatic for PRO subscribers)
- Add nostr:nprofile1qqsydl97xpj74udw0qg5vkfyujyjxd3l706jd0t0w0turp93d0vvungfgfewr to contributors
- Proxy LN address type enabled on Nostr Address settings pages
- Infinite scrol on notifications page
- Default 0.5% ZapPool rate for Snort donation address
- Collect relay metrics in `@snort/system` for better relay selection algo in Outbox Model (NIP-65)
- New sign up / login flow!
- Topics / Mute words on sign up for easier onboarding
- Drag & Drop for uploads on note creator - nostr:nprofile1qqs8tchhwf5smv3r2g0vkswz58c837uu456x59m3dh380gtrhqzydeqz4wlka
- Mixin topics (hashtags) into timeline feed
- Language specific trending posts
- Show following info for hashtags
- Sync preferences to network (`NIP-78` support)
- Trending hashtags page
- Note creator hashtag input
- Top trending hashtags on note creator
- Social Graph - nostr:nprofile1qqsy2ga7trfetvd3j65m3jptqw9k39wtq2mg85xz2w542p5dhg06e5qpr9mhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv9uh8am0r
- New users relay list based off "close" relays
- `NIP-96` support for nostr native image/file uploaders
- Write replies/reactions to `p` tagged users read relays (Outbox model)
- Sync joined public chats (`NIP-28`) using `PublicChatList` kind `10_005`
## Changed
- Read/Write relays only on kind `10_002` (NIP-65)
- Removed `nostr.watch` code for adding new users to random relays
- Render kind `10_002` on profile relays tab
- `@snort/system` using eventemitter3 for triggering events
- Use latest `NIP-51` spec (Bookmarks/Interests/`NIP-28` PublicChatList)
- `nreq` support (Demo)
- Write profile/relays to blasters
- `@snort/system` automated outbox model (automatic fetching of relay metadata)
## Fixes
- Upgrade ephermal connection to non-ephemeral
- Remove relay tag from zaps (Some zap services dont support it)
- Fix zap parsing for goals
- Remove extra chars from quoted events to fix loading (`'s` etc)
- CSS Fixes for profile card on light theme
- Zap counting on replacable events
- `NIP-28` chats loading
- Overflowing modal UI
- Live stream widget layout with long titles
- Notifications marker has returned from its long slumber
---
# v0.1.22
## Fixes

View File

@ -1,6 +1,6 @@
{
"name": "@snort/app",
"version": "0.1.22",
"version": "0.1.23",
"dependencies": {
"@cashu/cashu-ts": "^0.6.1",
"@lightninglabs/lnc-web": "^0.2.3-alpha",
@ -35,7 +35,6 @@
"react-router-dom": "^6.5.0",
"react-tag-input-component": "^2.0.2",
"react-textarea-autosize": "^8.4.0",
"react-twitter-embed": "^4.0.4",
"recharts": "^2.8.0",
"three": "^0.157.0",
"use-long-press": "^3.2.0",

View File

@ -93,11 +93,6 @@ export const InvoiceRegex = /(lnbc\w+)/i;
export const YoutubeUrlRegex =
/(?:https?:\/\/)?(?:www|m\.)?(?:youtu\.be\/|youtube\.com\/(?:live\/|shorts\/|embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/;
/**
* Tweet Regex
*/
export const TweetUrlRegex = /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
/**
* Hashtag regex
*/

View File

@ -51,7 +51,7 @@ export default function Note(props: NoteProps) {
if (ev.kind === EventKind.ZapstrTrack) {
return <ZapstrEmbed ev={ev} />;
}
if (ev.kind === EventKind.CategorizedPeople || ev.kind === EventKind.ContactList) {
if (ev.kind === EventKind.FollowSet || ev.kind === EventKind.ContactList) {
return <PubkeyList ev={ev} className={className} />;
}
if (ev.kind === EventKind.LiveEvent) {

View File

@ -1,8 +1,5 @@
// import { TwitterTweetEmbed } from "react-twitter-embed";
import {
YoutubeUrlRegex,
//TweetUrlRegex,
TidalRegex,
SoundCloudRegex,
MixCloudRegex,
@ -37,7 +34,6 @@ export default function HyperText({ link, depth, showLinkPreview, children }: Hy
try {
const url = new URL(a);
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
//const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
const tidalId = TidalRegex.test(a) && RegExp.$1;
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
@ -46,13 +42,8 @@ export default function HyperText({ link, depth, showLinkPreview, children }: Hy
const isAppleMusicLink = AppleMusicRegex.test(a);
const isNostrNestsLink = NostrNestsRegex.test(a);
const isWavlakeLink = WavlakeRegex.test(a);
/*if (tweetId) { // tmp disabled, react-twitter-embed causes "require is not defined" error
return (
<div className="tweet" key={tweetId}>
<TwitterTweetEmbed tweetId={tweetId} />
</div>
);
} else*/ if (youtubeId) {
if (youtubeId) {
return (
<iframe
className="w-max"

View File

@ -16,7 +16,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
switch (status) {
case "live": {
return (
<div className="flex g4">
<div className="flex g4 items-center">
<Icon name="signal-01" />
<b className="uppercase">
<FormattedMessage defaultMessage="Live" id="Dn82AL" />
@ -49,7 +49,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
case "live": {
return (
<Link to={link} target="_blank">
<button type="button">
<button className="nowrap">
<FormattedMessage defaultMessage="Join Stream" id="GQPtfk" />
</button>
</Link>
@ -59,7 +59,7 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
if (findTag(ev, "recording")) {
return (
<Link to={link} target="_blank">
<button type="button">
<button className="nowrap">
<FormattedMessage defaultMessage="Watch Replay" id="6/hB3S" />
</button>
</Link>
@ -70,13 +70,13 @@ export function LiveEvent({ ev }: { ev: NostrEvent }) {
}
return (
<div className="flex justify-between br p24 bg-primary">
<div className="flex g12">
<div className="sm:flex g12 br p24 bg-primary items-center">
<div>
<ProfileImage pubkey={host} showUsername={false} size={56} />
<div>
<h2>{title}</h2>
{statusLine()}
</div>
</div>
<div className="flex flex-col g8 grow">
<div className="font-semibold text-3xl">{title}</div>
<div>{statusLine()}</div>
</div>
<div>{cta()}</div>
</div>

View File

@ -20,6 +20,7 @@
margin-top: auto;
margin-bottom: auto;
--border-color: var(--gray);
max-width: 100%;
}
@media (min-width: 600px) {
@ -28,6 +29,11 @@
width: 600px;
}
}
@media (max-width: 600px) {
.modal-body {
min-width: 100%;
}
}
.modal-body button.secondary:hover {
background-color: var(--gray);

View File

@ -16,7 +16,7 @@ enum Provider {
}
export default function SuggestedProfiles() {
const login = useLogin();
const login = useLogin(s => ({ publicKey: s.publicKey, follows: s.follows.item }));
const [userList, setUserList] = useState<HexKey[]>();
const [provider, setProvider] = useState(Provider.NostrBand);
const [error, setError] = useState<Error>();
@ -37,7 +37,7 @@ export default function SuggestedProfiles() {
}
case Provider.SemisolDev: {
const api = new SemisolDevApi();
const users = await api.sugguestedFollows(login.publicKey, login.follows.item);
const users = await api.sugguestedFollows(login.publicKey, login.follows);
const keys = users.recommendations.sort(a => a[1]).map(a => a[0]);
setUserList(keys);
break;
@ -52,7 +52,7 @@ export default function SuggestedProfiles() {
useEffect(() => {
loadSuggestedProfiles();
}, [login, provider]);
}, [login.publicKey, login.follows, provider]);
return (
<>

View File

@ -1,13 +1,12 @@
import "./ProfileImage.css";
import React, { ReactNode, useCallback, useRef, useState } from "react";
import { HexKey, socialGraphInstance, UserMetadata } from "@snort/system";
import { HexKey, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import classNames from "classnames";
import Avatar from "@/Element/User/Avatar";
import Nip05 from "@/Element/User/Nip05";
import Icon from "@/Icons/Icon";
import DisplayName from "./DisplayName";
import { ProfileLink } from "./ProfileLink";
import { ProfileCard } from "./ProfileCard";

View File

@ -1,13 +1,12 @@
import { useEffect, useMemo } from "react";
import { TaggedNostrEvent, EventKind, RequestBuilder, NoteCollection, NostrLink } from "@snort/system";
import { TaggedNostrEvent, EventKind, RequestBuilder, NoteCollection, NostrLink, parseRelayTags } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { bech32ToHex, debounce, findTag, getNewest, getNewestEventTagsByKey, unwrap } from "@/SnortUtils";
import { makeNotification, sendNotification } from "@/Notifications";
import { bech32ToHex, debounce, getNewest, getNewestEventTagsByKey, unwrap } from "@/SnortUtils";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useModeration from "@/Hooks/useModeration";
import useLogin from "@/Hooks/useLogin";
import {
LoginStore,
SnortAppData,
addSubscription,
setAppData,
@ -21,18 +20,18 @@ import {
} from "@/Login";
import { SnortPubKey } from "@/Const";
import { SubscriptionEvent } from "@/Subscription";
import useRelaysFeedFollows from "./RelaysFeedFollows";
import { FollowLists, FollowsFeed, GiftsCache, Notifications, UserRelays } from "@/Cache";
import { Nip28Chats, Nip4Chats } from "@/chat";
import { useRefreshFeedCache } from "@/Hooks/useRefreshFeedcache";
import { usePrevious } from "@uidotdev/usehooks";
import { Nip28ChatSystem } from "@/chat/nip28";
/**
* Managed loading data for the current logged in user
*/
export default function useLoginFeed() {
const login = useLogin();
const { publicKey: pubKey, readNotifications, follows } = login;
const { isMuted } = useModeration();
const { publicKey: pubKey, follows } = login;
const { publisher, system } = useEventPublisher();
useRefreshFeedCache(Notifications, true);
@ -44,15 +43,19 @@ export default function useLoginFeed() {
system.checkSigs = login.appData.item.preferences.checkSigs;
}, [login]);
const previous = usePrevious(login.appData.item);
// write appdata after 10s of no changes
useEffect(() => {
if (!previous || JSON.stringify(previous) === JSON.stringify(login.appData.item)) {
return;
}
return debounce(10_000, async () => {
if (publisher && login.appData.item) {
const ev = await publisher.appData(login.appData.item, "snort");
await system.BroadcastEvent(ev);
}
});
}, [login.appData.timestamp]);
}, [previous]);
const subLogin = useMemo(() => {
if (!login || !pubKey) return null;
@ -63,8 +66,16 @@ export default function useLoginFeed() {
});
b.withFilter()
.authors([pubKey])
.kinds([EventKind.ContactList, EventKind.Relays, EventKind.MuteList, EventKind.PinList]);
b.withFilter().authors([pubKey]).kinds([EventKind.CategorizedBookmarks]).tag("d", ["follow", "bookmark"]);
.kinds([
EventKind.ContactList,
EventKind.Relays,
EventKind.MuteList,
EventKind.PinList,
EventKind.BookmarksList,
EventKind.InterestsList,
EventKind.PublicChatsList,
]);
b.withFilter().authors([pubKey]).kinds([]);
if (CONFIG.features.subscriptions && !login.readonly) {
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
b.withFilter()
@ -101,17 +112,7 @@ export default function useLoginFeed() {
const relays = getNewest(loginFeed.data.filter(a => a.kind === EventKind.Relays));
if (relays) {
const parsedRelays = relays.tags
.filter(a => a[0] === "r")
.map(a => {
return [
a[1],
{
read: a[2] === "read" || a[2] === undefined,
write: a[2] === "write" || a[2] === undefined,
},
];
});
const parsedRelays = parseRelayTags(relays.tags.filter(a => a[0] === "r")).map(a => [a.url, a.settings]);
setRelays(login, Object.fromEntries(parsedRelays), relays.created_at * 1000);
}
@ -145,21 +146,6 @@ export default function useLoginFeed() {
}
}, [loginFeed, publisher]);
// send out notifications
useEffect(() => {
if (loginFeed.data) {
const replies = loginFeed.data.filter(
a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications,
);
replies.forEach(async nx => {
const n = await makeNotification(nx);
if (n) {
sendNotification(login, n);
}
});
}
}, [loginFeed, readNotifications]);
async function handleMutedFeed(mutedFeed: TaggedNostrEvent[]) {
const latest = getNewest(mutedFeed);
if (!latest) return;
@ -204,6 +190,16 @@ export default function useLoginFeed() {
}
}
function handlePublicChatsListFeed(bookmarkFeed: TaggedNostrEvent[]) {
const newest = getNewestEventTagsByKey(bookmarkFeed, "e");
if (newest) {
LoginStore.updateSession({
...login,
extraChats: newest.keys.map(Nip28ChatSystem.chatId),
});
}
}
useEffect(() => {
if (loginFeed.data) {
const mutedFeed = loginFeed.data.filter(a => a.kind === EventKind.MuteList);
@ -212,25 +208,19 @@ export default function useLoginFeed() {
const pinnedFeed = loginFeed.data.filter(a => a.kind === EventKind.PinList);
handlePinnedFeed(pinnedFeed);
const tagsFeed = loginFeed.data.filter(
a => a.kind === EventKind.CategorizedBookmarks && findTag(a, "d") === "follow",
);
const tagsFeed = loginFeed.data.filter(a => a.kind === EventKind.InterestsList);
handleTagFeed(tagsFeed);
const bookmarkFeed = loginFeed.data.filter(
a => a.kind === EventKind.CategorizedBookmarks && findTag(a, "d") === "bookmark",
);
const bookmarkFeed = loginFeed.data.filter(a => a.kind === EventKind.BookmarksList);
handleBookmarkFeed(bookmarkFeed);
const publicChatsFeed = loginFeed.data.filter(a => a.kind === EventKind.PublicChatsList);
handlePublicChatsListFeed(publicChatsFeed);
}
}, [loginFeed]);
useEffect(() => {
UserRelays.buffer(follows.item).catch(console.error);
system.ProfileLoader.TrackMetadata(follows.item); // always track follows profiles
system.ProfileLoader.TrackKeys(follows.item); // always track follows profiles
}, [follows.item]);
const fRelays = useRelaysFeedFollows(follows.item);
useEffect(() => {
UserRelays.bulkSet(fRelays).catch(console.error);
}, [fRelays]);
}

View File

@ -1,7 +1,6 @@
import { useMemo } from "react";
import { HexKey, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system";
import { HexKey, EventKind, RequestBuilder, ReplaceableNoteStore, parseRelayTags } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { parseRelayTag } from "./RelaysFeedFollows";
export default function useRelaysFeed(pubkey?: HexKey) {
const sub = useMemo(() => {
@ -12,5 +11,5 @@ export default function useRelaysFeed(pubkey?: HexKey) {
}, [pubkey]);
const relays = useRequestBuilder(ReplaceableNoteStore, sub);
return relays.data?.tags.filter(a => a[0] === "r").map(parseRelayTag) ?? [];
return parseRelayTags(relays.data?.tags.filter(a => a[0] === "r") ?? []);
}

View File

@ -1,49 +0,0 @@
import { useMemo } from "react";
import { HexKey, FullRelaySettings, TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import debug from "debug";
import { sanitizeRelayUrl } from "@/SnortUtils";
import { UserRelays } from "@/Cache";
interface RelayList {
pubkey: string;
created_at: number;
relays: FullRelaySettings[];
}
export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array<RelayList> {
const sub = useMemo(() => {
const b = new RequestBuilder(`relays:follows`);
const since = UserRelays.newest();
debug("LoginFeed")("Loading relay lists since %s", new Date(since * 1000).toISOString());
b.withFilter().authors(pubkeys).kinds([EventKind.Relays]).since(since);
return b;
}, [pubkeys]);
function mapFromRelays(notes: Array<TaggedNostrEvent>): Array<RelayList> {
return notes.map(ev => {
return {
pubkey: ev.pubkey,
created_at: ev.created_at,
relays: ev.tags.map(parseRelayTag).filter(a => a.url !== undefined),
};
});
}
const relays = useRequestBuilder(NoteCollection, sub);
const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? [];
return useMemo(() => {
return mapFromRelays(notesRelays);
}, [relays]);
}
export function parseRelayTag(tag: Array<string>) {
return {
url: sanitizeRelayUrl(tag[1]),
settings: {
read: tag[2] === "read" || tag[2] === undefined,
write: tag[2] === "write" || tag[2] === undefined,
},
} as FullRelaySettings;
}

View File

@ -27,7 +27,7 @@ export function useLinkListEvents(id: string, fn: (rb: RequestBuilder) => void)
}
export function usePinList(pubkey: string | undefined) {
return useLinkListEvents(`pins:${pubkey?.slice(0, 12)}`, rb => {
return useLinkListEvents(`list:pins:${pubkey?.slice(0, 12)}`, rb => {
if (pubkey) {
rb.withFilter().kinds([EventKind.PinList]).authors([pubkey]);
}
@ -35,17 +35,25 @@ export function usePinList(pubkey: string | undefined) {
}
export function useMuteList(pubkey: string | undefined) {
return useLinkList(`pins:${pubkey?.slice(0, 12)}`, rb => {
return useLinkList(`list:mute:${pubkey?.slice(0, 12)}`, rb => {
if (pubkey) {
rb.withFilter().kinds([EventKind.MuteList]).authors([pubkey]);
}
});
}
export default function useCategorizedBookmarks(pubkey: string | undefined, list: string) {
return useLinkListEvents(`categorized-bookmarks:${list}:${pubkey?.slice(0, 12)}`, rb => {
export function useBookmarkList(pubkey: string | undefined) {
return useLinkListEvents(`list:bookmark:${pubkey?.slice(0, 12)}`, rb => {
if (pubkey) {
rb.withFilter().kinds([EventKind.CategorizedBookmarks]).authors([pubkey]).tag("d", [list]);
rb.withFilter().kinds([EventKind.BookmarksList]).authors([pubkey]);
}
});
}
export function useInterestsList(pubkey: string | undefined) {
return useLinkList(`list:interest:${pubkey?.slice(0, 12)}`, rb => {
if (pubkey) {
rb.withFilter().kinds([EventKind.InterestsList]).authors([pubkey]);
}
});
}

View File

@ -95,11 +95,6 @@ export interface LoginSession {
*/
blocked: Newest<Array<HexKey>>;
/**
* Latest notification
*/
latestNotification: number;
/**
* Timestamp of last read notification
*/

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Link, useParams } from "react-router-dom";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { EventKind, NostrHashtagLink, NoteCollection, RequestBuilder } from "@snort/system";
import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { dedupe } from "@snort/shared";
import { useRequestBuilder } from "@snort/system-react";
@ -44,10 +44,11 @@ export function HashTagHeader({ tag, events, className }: { tag: string; events?
async function followTags(ts: string[]) {
if (publisher) {
const ev = await publisher.bookmarks(
ts.map(a => new NostrHashtagLink(a)),
"follow",
);
const ev = await publisher.generic(eb => {
eb.kind(EventKind.InterestsList);
ts.forEach(a => eb.tag(["t", a]));
return eb;
});
setTags(login, ts, ev.created_at * 1000);
await system.BroadcastEvent(ev);
}
@ -55,7 +56,7 @@ export function HashTagHeader({ tag, events, className }: { tag: string; events?
const sub = useMemo(() => {
const rb = new RequestBuilder(`hashtag-counts:${tag}`);
rb.withFilter().kinds([EventKind.CategorizedBookmarks]).tag("d", ["follow"]).tag("t", [tag.toLowerCase()]);
rb.withFilter().kinds([EventKind.InterestsList]).tag("t", [tag.toLowerCase()]);
return rb;
}, [tag]);
const followsTag = useRequestBuilder(NoteCollection, sub);

View File

@ -1,18 +1,19 @@
import { Link, useNavigate } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import { useMemo } from "react";
import { useMemo, useSyncExternalStore } from "react";
import { base64 } from "@scure/base";
import { unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
import SearchBox from "../../Element/SearchBox";
import { ProfileLink } from "../../Element/User/ProfileLink";
import Avatar from "../../Element/User/Avatar";
import Icon from "../../Icons/Icon";
import useKeyboardShortcut from "../../Hooks/useKeyboardShortcut";
import { isFormElement } from "../../SnortUtils";
import useLogin from "../../Hooks/useLogin";
import useEventPublisher from "../../Hooks/useEventPublisher";
import SnortApi from "../../External/SnortApi";
import SearchBox from "@/Element/SearchBox";
import { ProfileLink } from "@/Element/User/ProfileLink";
import Avatar from "@/Element/User/Avatar";
import Icon from "@/Icons/Icon";
import useKeyboardShortcut from "@/Hooks/useKeyboardShortcut";
import { isFormElement } from "@/SnortUtils";
import useLogin from "@/Hooks/useLogin";
import useEventPublisher from "@/Hooks/useEventPublisher";
import SnortApi from "@/External/SnortApi";
import { Notifications } from "@/Cache";
const AccountHeader = () => {
const navigate = useNavigate();
@ -25,19 +26,13 @@ const AccountHeader = () => {
}
});
const { publicKey, latestNotification, readNotifications, readonly } = useLogin(s => ({
const { publicKey, readonly } = useLogin(s => ({
publicKey: s.publicKey,
latestNotification: s.latestNotification,
readNotifications: s.readNotifications,
readonly: s.readonly,
}));
const profile = useUserProfile(publicKey);
const { publisher } = useEventPublisher();
const hasNotifications = useMemo(
() => latestNotification > readNotifications,
[latestNotification, readNotifications],
);
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
async function goToNotifications() {
@ -92,7 +87,7 @@ const AccountHeader = () => {
)}
<Link className="btn" to="/notifications" onClick={goToNotifications}>
<Icon name="bell-02" size={24} />
{hasNotifications && <span className="has-unread"></span>}
<HasNotificationsMarker />
</Link>
<ProfileLink pubkey={publicKey} user={profile}>
<Avatar pubkey={publicKey} user={profile} />
@ -101,4 +96,24 @@ const AccountHeader = () => {
);
};
function HasNotificationsMarker() {
const readNotifications = useLogin(s => s.readNotifications);
const notifications = useSyncExternalStore(
c => Notifications.hook(c, "*"),
() => Notifications.snapshot(),
);
const latestNotification = useMemo(
() => notifications.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
[notifications],
);
const hasNotifications = useMemo(
() => latestNotification * 1000 > readNotifications,
[notifications, readNotifications],
);
if (hasNotifications) {
return <span className="has-unread"></span>;
}
}
export default AccountHeader;

View File

@ -48,9 +48,9 @@ header {
border-radius: 100%;
width: 9px;
height: 9px;
position: absolute;
top: 8px;
right: 8px;
position: relative;
top: -4px;
right: 7px;
}
@media (max-width: 520px) {

View File

@ -3,7 +3,7 @@ import "./MessagesPage.css";
import React, { useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import { NostrLink, TLVEntryType, UserMetadata, decodeTLV } from "@snort/system";
import { EventKind, NostrLink, TLVEntryType, UserMetadata, decodeTLV } from "@snort/system";
import { useEventFeed, useUserProfile, useUserSearch } from "@snort/system-react";
import UnreadCount from "@/Element/UnreadCount";
@ -25,6 +25,7 @@ import { LoginSession, LoginStore } from "@/Login";
import { Nip28ChatSystem } from "@/chat/nip28";
import { ChatParticipantProfile } from "@/Element/Chat/ChatParticipant";
import classNames from "classnames";
import useEventPublisher from "@/Hooks/useEventPublisher";
const TwoCol = 768;
const ThreeCol = 1500;
@ -182,6 +183,7 @@ function NewChatWindow() {
const navigate = useNavigate();
const search = useUserSearch();
const login = useLogin();
const { system, publisher } = useEventPublisher();
useEffect(() => {
setNewChat([]);
@ -270,12 +272,25 @@ function NewChatWindow() {
{results.length === 1 && (
<Nip28ChatProfile
id={results[0]}
onClick={id => {
onClick={async id => {
setShow(false);
const chats = appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]);
LoginStore.updateSession({
...login,
extraChats: appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]),
extraChats: chats,
} as LoginSession);
const evList = await publisher?.generic(eb => {
eb.kind(EventKind.PublicChatsList);
chats.forEach(c => {
if (c.startsWith("chat281")) {
eb.tag(["e", decodeTLV(c)[0].value as string]);
}
});
return eb;
});
if (evList) {
await system.BroadcastEvent(evList);
}
navigate(createChatLink(ChatType.PublicGroupChat, id));
}}
/>

View File

@ -13,7 +13,7 @@ import Bookmarks from "@/Element/User/Bookmarks";
import Icon from "@/Icons/Icon";
import { Tab } from "@/Element/Tabs";
import { default as ZapElement } from "@/Element/Event/Zap";
import useCategorizedBookmarks from "@/Hooks/useLists";
import { useBookmarkList } from "@/Hooks/useLists";
import messages from "../messages";
@ -60,7 +60,7 @@ export function RelaysTab({ id }: { id: HexKey }) {
}
export function BookMarksTab({ id }: { id: HexKey }) {
const bookmarks = useCategorizedBookmarks(id, "bookmark");
const bookmarks = useBookmarkList(id);
const reactions = useReactions(`bookmark:reactions:{id}`, bookmarks.map(NostrLink.fromEvent));
return <Bookmarks pubkey={id} bookmarks={bookmarks} related={reactions.data ?? []} />;
}

View File

@ -5,7 +5,7 @@ import AsyncButton from "@/Element/AsyncButton";
import classNames from "classnames";
import { appendDedupe } from "@/SnortUtils";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { NostrHashtagLink } from "@snort/system";
import { EventKind } from "@snort/system";
export const FixedTopics = {
life: {
@ -283,11 +283,14 @@ export function Topics() {
const tags = Object.entries(FixedTopics)
.filter(([k]) => topics.includes(k))
.map(([, v]) => v.tags)
.flat()
.map(a => new NostrHashtagLink(a));
.flat();
if (tags.length > 0) {
const ev = await publisher?.bookmarks(tags, "follow");
const ev = await publisher?.generic(eb => {
eb.kind(EventKind.InterestsList);
tags.forEach(a => eb.tag(["t", a]));
return eb;
});
if (ev) {
await system.BroadcastEvent(ev);
}

View File

@ -0,0 +1,53 @@
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { BaseUITask } from "@/Tasks";
import { MetadataCache } from "@snort/system";
import { LoginSession } from "@/Login";
import Icon from "@/Icons/Icon";
export class BackupKeyTask extends BaseUITask {
id = "backup-key";
noBaseStyle = true;
check(_: MetadataCache, session: LoginSession): boolean {
return !this.state.muted && session.type == "private_key";
}
render() {
return (
<div className="p card">
<div className="flex g12 bg-superdark p24 br">
<div>
<div className="p12 bg-dark circle">
<Icon name="key" size={21} />
</div>
</div>
<div className="flex flex-col g8">
<div className="font-semibold text-xl">
<FormattedMessage defaultMessage="Be sure to back up your keys!" id="1UWegE" />
</div>
<small>
<FormattedMessage
defaultMessage="No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute."
id="YR2I9M"
values={{
app: CONFIG.appNameCapitalized,
}}
/>
</small>
<div className="flex g8">
<Link to="/settings/keys">
<button>
<FormattedMessage defaultMessage="Back up now" id="rMgF34" />
</button>
</Link>
<button className="secondary" onClick={() => this.mute()}>
<FormattedMessage defaultMessage="Already backed up" id="j9xbzF" />
</button>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import "./TaskList.css";
import { useState } from "react";
import { useSyncExternalStore } from "react";
import { useUserProfile } from "@snort/system-react";
import useLogin from "@/Hooks/useLogin";
@ -9,38 +9,65 @@ import { DonateTask } from "./DonateTask";
import { Nip5Task } from "./Nip5Task";
import { RenewSubTask } from "./RenewSubscription";
import { NoticeZapPoolDefault } from "./NoticeZapPool";
import { BackupKeyTask } from "./BackupKey";
import { ExternalStore } from "@snort/shared";
const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask(), new NoticeZapPoolDefault()];
if (CONFIG.features.subscriptions) {
AllTasks.push(new RenewSubTask());
class TaskStore extends ExternalStore<Array<UITask>> {
#tasks: Array<UITask>;
constructor() {
super();
const AllTasks: Array<UITask> = [new BackupKeyTask(), new Nip5Task(), new DonateTask(), new NoticeZapPoolDefault()];
if (CONFIG.features.subscriptions) {
AllTasks.push(new RenewSubTask());
}
AllTasks.forEach(a =>
a.load(() => {
this.notifyChange();
}),
);
this.#tasks = AllTasks;
}
takeSnapshot(): UITask[] {
return [...this.#tasks];
}
}
AllTasks.forEach(a => a.load());
const AllTasks = new TaskStore();
export const TaskList = () => {
const session = useLogin();
const user = useUserProfile(session.publicKey);
const [, setTick] = useState<number>(0);
const tasks = useSyncExternalStore(
c => AllTasks.hook(c),
() => AllTasks.snapshot(),
);
function muteTask(t: UITask) {
t.mute();
setTick(x => (x += 1));
}
return (
<div className="task-list">
{AllTasks.filter(a => (user ? a.check(user, session) : false)).map(a => {
return (
<div key={a.id} className="card">
<div className="header">
<Icon name="lightbulb" />
<div className="close" onClick={() => muteTask(a)}>
<Icon name="close" size={14} />
{tasks
.filter(a => (user ? a.check(user, session) : false))
.map(a => {
if (a.noBaseStyle) {
return a.render();
} else {
return (
<div key={a.id} className="card">
<div className="header">
<Icon name="lightbulb" />
<div className="close" onClick={() => muteTask(a)}>
<Icon name="close" size={14} />
</div>
</div>
{a.render()}
</div>
</div>
{a.render()}
</div>
);
})}
);
}
})}
</div>
);
};

View File

@ -3,12 +3,13 @@ import { LoginSession } from "@/Login";
export interface UITask {
id: string;
noBaseStyle: boolean;
/**
* Run checks to determine if this Task should be triggered for this user
*/
check(user: MetadataCache, session: LoginSession): boolean;
mute(): void;
load(): void;
load(cb: () => void): void;
render(): JSX.Element;
}
@ -19,9 +20,11 @@ export interface UITaskState {
}
export abstract class BaseUITask implements UITask {
#cb?: () => void;
protected state: UITaskState;
abstract id: string;
noBaseStyle = false;
abstract check(user: MetadataCache, session: LoginSession): boolean;
abstract render(): JSX.Element;
@ -34,7 +37,8 @@ export abstract class BaseUITask implements UITask {
this.#save();
}
load() {
load(cb: () => void) {
this.#cb = cb;
const state = window.localStorage.getItem(`task:${this.id}`);
if (state) {
this.state = JSON.parse(state);
@ -43,5 +47,6 @@ export abstract class BaseUITask implements UITask {
#save() {
window.localStorage.setItem(`task:${this.id}`, JSON.stringify(this.state));
this.#cb?.();
}
}

View File

@ -204,6 +204,6 @@ export function useChatSystem() {
return [...nip4, ...nip28].filter(a => {
const authors = a.participants.filter(a => a.type === "pubkey").map(a => a.id);
return !authors.every(a => isBlocked(a));
return authors.length === 0 || !authors.every(a => isBlocked(a));
});
}

View File

@ -1,5 +1,5 @@
import debug from "debug";
import { ExternalStore, FeedCache, unixNow, unwrap } from "@snort/shared";
import { ExternalStore, FeedCache, unwrap } from "@snort/shared";
import {
EventKind,
NostrEvent,
@ -16,7 +16,6 @@ import {
import { LoginSession } from "@/Login";
import { findTag } from "@/SnortUtils";
import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { Day } from "@/Const";
export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
#cache: FeedCache<NostrEvent>;
@ -50,7 +49,7 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
const lastMessage = messages[id]?.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0) ?? 0;
rb.withFilter()
.tag("e", [id])
.since(lastMessage === 0 ? unixNow() - 2 * Day : lastMessage)
.since(lastMessage === 0 ? undefined : lastMessage)
.kinds(this.ChannelKinds);
}
@ -67,9 +66,10 @@ export class Nip28ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
listChats(): Chat[] {
const chats = this.#chatChannels();
return Object.entries(chats).map(([k, v]) => {
const ret = Object.entries(chats).map(([k, v]) => {
return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v);
});
return ret;
}
static chatId(id: string) {

View File

@ -710,24 +710,6 @@ div.form-col {
color: var(--gray-light);
}
.tweet {
display: flex;
align-items: center;
justify-content: center;
}
.tweet div {
width: 100%;
}
.tweet div .twitter-tweet {
margin: 0 auto;
}
.tweet div .twitter-tweet > iframe {
max-height: unset;
}
@media (max-width: 720px) {
div.form {
grid-auto-flow: dense;

View File

@ -90,6 +90,9 @@
"1R43+L": {
"defaultMessage": "Enter Nostr Wallet Connect config"
},
"1UWegE": {
"defaultMessage": "Be sure to back up your keys!"
},
"1c4YST": {
"defaultMessage": "Connected to: {node} 🎉"
},
@ -913,6 +916,9 @@
"YDURw6": {
"defaultMessage": "Service URL"
},
"YR2I9M": {
"defaultMessage": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute."
},
"YXA3AH": {
"defaultMessage": "Enable reactions"
},
@ -1170,6 +1176,9 @@
"izWS4J": {
"defaultMessage": "Unfollow"
},
"j9xbzF": {
"defaultMessage": "Already backed up"
},
"jA3OE/": {
"defaultMessage": "{n,plural,=1{{n} sat} other{{n} sats}}"
},
@ -1358,6 +1367,9 @@
"r5srDR": {
"defaultMessage": "Enter wallet password"
},
"rMgF34": {
"defaultMessage": "Back up now"
},
"rT14Ow": {
"defaultMessage": "Add Relays"
},

View File

@ -17,6 +17,21 @@ self.addEventListener("message", event => {
self.skipWaiting();
}
});
self.addEventListener("install", event => {
// delete all cache on install
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
console.debug("Deleting cache: ", cacheName);
return caches.delete(cacheName);
}),
);
}),
);
// always skip waiting
self.skipWaiting();
});
const enum PushType {
Mention = 1,

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "هل أنت متأكد من حذف هذا المنشور من المنشورات المرجعية؟",
"1R43+L": "أدخل تكوين اتصال محفظة Nostr",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "متصل بـ: {node}🎉",
"1nYUGC": "المتابَعون {n}",
"1o2BgB": "التحقق من التوقيعات",
@ -298,6 +299,7 @@
"Xopqkl": "مبلغ زابي الافتراضي الخاص بك هو {number} جلسة، على سبيل المثال يتم حساب القيم من هذا.",
"XrSk2j": "Redeem",
"YDURw6": "رابط الخدمة",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "تمكين التفاعل",
"Z4BMCZ": "أدخل عبارة الاقتران",
"ZKORll": "نشط الآن",
@ -381,6 +383,7 @@
"ieGrWo": "متابعة",
"itPgxd": "الملف التعريفي",
"izWS4J": "الغاء المتابعة",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} ساتوشي}other{{n} ساتوشي}}",
"jAmfGl": "إنتهت صلاحية اشتراكك {site_name}",
"jHa/ko": "تنظيف موجز الويب الخاص بك",
@ -442,6 +445,7 @@
"qz9fty": "دبوس غير صحيح",
"r3C4x/": "برنامج",
"r5srDR": "أدخل كلمة مرور المحفظة",
"rMgF34": "Back up now",
"rT14Ow": "إضافة موصّلات",
"rbrahO": "أغلق",
"rfuMjE": "(افتراضي)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Bu qeydi əlfəcinlərdən silmək istədiyinizə əminsiniz?",
"1R43+L": "Nostr Wallet Connect konfiqurasiyasını daxil edin",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Qoşuldu: {node} 🎉",
"1nYUGC": "{n} İzləyir",
"1o2BgB": "Check Signatures",
@ -298,6 +299,7 @@
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"YDURw6": "Service URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Enable reactions",
"Z4BMCZ": "Enter pairing phrase",
"ZKORll": "Activate Now",
@ -381,6 +383,7 @@
"ieGrWo": "Follow",
"itPgxd": "Profile",
"izWS4J": "Unfollow",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -442,6 +445,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Enter wallet password",
"rMgF34": "Back up now",
"rT14Ow": "Add Relays",
"rbrahO": "Close",
"rfuMjE": "(Default)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} Benutzer",
"1Mo59U": "Bist du sicher, dass du diese Note aus deinen Lesezeichen entfernen möchtest?",
"1R43+L": "Nostr Wallet Connect Konfiguration eingeben",
"1UWegE": "Stelle sicher, dass du deine Schlüssel absicherst!",
"1c4YST": "Verbunden mit: {node}🎉",
"1nYUGC": "{n} Folgen",
"1o2BgB": "Signaturen prüfen",
@ -298,6 +299,7 @@
"Xopqkl": "Dein standardmäßiger Zap-Betrag ist {number} sats, Beispielwerte werden daraus berechnet.",
"XrSk2j": "Einlösen",
"YDURw6": "URL des Dienstes",
"YR2I9M": "Keine Schlüssel, kein {app}. Es gibt keine Möglichkeit, sie zurückzusetzen, wenn du keine Sicherungskopie gemacht hast. Es dauert nur eine Minute.",
"YXA3AH": "Reaktionen aktivieren",
"Z4BMCZ": "Verbindungs-Passphrase eingeben",
"ZKORll": "Jetzt aktivieren",
@ -381,6 +383,7 @@
"ieGrWo": "Folgen",
"itPgxd": "Profil",
"izWS4J": "Entfolgen",
"j9xbzF": "Bereits gesichert",
"jA3OE/": "{n,plural,one {}=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Dein {site_name} Abonnement ist abgelaufen",
"jHa/ko": "Räume deinen Feed auf",
@ -442,6 +445,7 @@
"qz9fty": "Falsche PIN",
"r3C4x/": "Software",
"r5srDR": "Wallet Passwort eingeben",
"rMgF34": "Jetzt sichern",
"rT14Ow": "Relais hinzufügen",
"rbrahO": "Schließen",
"rfuMjE": "(Standard)",

View File

@ -29,6 +29,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Are you sure you want to remove this note from bookmarks?",
"1R43+L": "Enter Nostr Wallet Connect config",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Connected to: {node} 🎉",
"1nYUGC": "{n} Following",
"1o2BgB": "Check Signatures",
@ -300,6 +301,7 @@
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"YDURw6": "Service URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Enable reactions",
"Z4BMCZ": "Enter pairing phrase",
"ZKORll": "Activate Now",
@ -385,6 +387,7 @@
"ieGrWo": "Follow",
"itPgxd": "Profile",
"izWS4J": "Unfollow",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -447,6 +450,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Enter wallet password",
"rMgF34": "Back up now",
"rT14Ow": "Add Relays",
"rbrahO": "Close",
"rfuMjE": "(Default)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "¿Estás seguro de que quieres eliminar esta nota de tus favoritos?",
"1R43+L": "Introduzca la configuración de Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Conectado a: {node}🎉",
"1nYUGC": "{n} Siguiendo",
"1o2BgB": "Firmas de cheques",
@ -298,6 +299,7 @@
"Xopqkl": "Tu cantidad de zap por defecto es {number} sats, los valores de ejemplo se calculan a partir de esto.",
"XrSk2j": "Canjear",
"YDURw6": "URL del Servicio",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Activar reacciones",
"Z4BMCZ": "Introduce la frase de emparejamiento",
"ZKORll": "Activar Ahora",
@ -381,6 +383,7 @@
"ieGrWo": "Seguir",
"itPgxd": "Perfil",
"izWS4J": "No seguir",
"j9xbzF": "Already backed up",
"jA3OE/": "{n} {n, plural, =1 {sat} other {sats}}",
"jAmfGl": "Su suscripción a {site_name} ha caducado",
"jHa/ko": "Limpie su alimentación",
@ -442,6 +445,7 @@
"qz9fty": "Clavija incorrecta",
"r3C4x/": "Software",
"r5srDR": "Introducir contraseña de cartera",
"rMgF34": "Back up now",
"rT14Ow": "Añadir Relays",
"rbrahO": "Cerrar",
"rfuMjE": "(Por defecto)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "مطمئنید می خواهید این یادداشت را از نشانک ها خارج کنید؟",
"1R43+L": "پیکربندی اتصال ناستر به کیف پول",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "متصل به:{node}🎉",
"1nYUGC": "{n} دنبال شونده",
"1o2BgB": "بررسی امضا",
@ -298,6 +299,7 @@
"Xopqkl": "مبلغ پیش فرض زپ شما {number} ساتوشی است، مقادیر نمونه از روی این محاسبه شده اند.",
"XrSk2j": "بازخرید",
"YDURw6": "خدمات URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "فعال سازی واکنش ها",
"Z4BMCZ": "عبارت جفت را وارد کنید",
"ZKORll": "اکنون فعال کن",
@ -381,6 +383,7 @@
"ieGrWo": "دنبال کردن",
"itPgxd": "نمایه",
"izWS4J": "لغو دنبال کردن",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,one {}=1{{n} نوت جدید} other{{n} نوت جدید}}",
"jAmfGl": "عضویت شما در {site_name} منقضی شده است",
"jHa/ko": "پاک سازی خبرنامه",
@ -442,6 +445,7 @@
"qz9fty": "پین نادرست",
"r3C4x/": "نرم افزار",
"r5srDR": "رمز کیف‌پول را وارد کنید",
"rMgF34": "Back up now",
"rT14Ow": "افزودن رله",
"rbrahO": "بستن",
"rfuMjE": "(پیش‌فرض)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Haluatko varmasti poistaa tämän viestin kirjanmerkeistä?",
"1R43+L": "Anna Nostr Wallet Connect asetukset",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Yhdistetty: {node} 🎉",
"1nYUGC": "{n} Seuraa",
"1o2BgB": "Tarkista allekirjoitukset",
@ -298,6 +299,7 @@
"Xopqkl": "Oletus zap-määräsi on {number} satsia, esimerkit on laskettu tämän mukaan.",
"XrSk2j": "Lunasta",
"YDURw6": "Palvelun URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Ota reaktiot käyttöön",
"Z4BMCZ": "Anna pariliitoslause",
"ZKORll": "Aktivoi nyt",
@ -381,6 +383,7 @@
"ieGrWo": "Seuraa",
"itPgxd": "Profiili",
"izWS4J": "Lopeta seuraaminen",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} satia}}",
"jAmfGl": "{site_name} tilauksesi on päättynyt",
"jHa/ko": "Siivoa rehusi",
@ -442,6 +445,7 @@
"qz9fty": "Väärä tappi",
"r3C4x/": "Ohjelmisto",
"r5srDR": "Anna lompakon salasana",
"rMgF34": "Back up now",
"rT14Ow": "Lisää välittäjiä",
"rbrahO": "Sulje",
"rfuMjE": "(Oletus)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Êtes-vous sûr de vouloir supprimer cette note de vos favoris ?",
"1R43+L": "Accéder à la configuration de Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Connecté à : {node} 🎉",
"1nYUGC": "{n} Abonnements",
"1o2BgB": "Signatures de chèques",
@ -298,6 +299,7 @@
"Xopqkl": "Votre montant de zap par défaut est {number} sats, les valeurs d'exemple sont calculées à partir de ceci.",
"XrSk2j": "Retirer",
"YDURw6": "URL de service",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Activer les réactions",
"Z4BMCZ": "Entrez la phrase d'appairage",
"ZKORll": "Activer Maintenant",
@ -381,6 +383,7 @@
"ieGrWo": "Suivre",
"itPgxd": "Profil",
"izWS4J": "Ne plus suivre",
"j9xbzF": "Already backed up",
"jA3OE/": "{n} {n, plural, =1 {sat} other {sats}}",
"jAmfGl": "Votre abonnement à {site_name} a expiré",
"jHa/ko": "Nettoyez votre flux",
@ -442,6 +445,7 @@
"qz9fty": "Broche incorrecte",
"r3C4x/": "Logiciel",
"r5srDR": "Entrez le mot de passe du portefeuille",
"rMgF34": "Back up now",
"rT14Ow": "Ajouter Relais",
"rbrahO": "Fermer",
"rfuMjE": "(Défaut)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Jeste li sigurni da želite ukloniti ovu bilješku iz trake s oznakama?",
"1R43+L": "Enter Nostr Wallet Connect config",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Povezano s: {node} 🎉",
"1nYUGC": "{n} Pratitelja",
"1o2BgB": "Check Signatures",
@ -298,6 +299,7 @@
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"YDURw6": "URL usluge",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Omogući reakcije",
"Z4BMCZ": "Unesite frazu za uparivanje",
"ZKORll": "Aktivirajte Odmah",
@ -381,6 +383,7 @@
"ieGrWo": "Prati",
"itPgxd": "Profil",
"izWS4J": "Prestani pratiti",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -442,6 +445,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "Software",
"r5srDR": "Unesite lozinku novčanika",
"rMgF34": "Back up now",
"rT14Ow": "Dodaj relej",
"rbrahO": "Close",
"rfuMjE": "(Zadano)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} felhasználó",
"1Mo59U": "Biztos hogy a kedvencekből ezt a bejegyzést el akarod távolítani?",
"1R43+L": "Írd be a Nostr Wallet Connect konfigurációt",
"1UWegE": "A kulcsaidról, mindenképpen készíts biztonsági másolatot!",
"1c4YST": "Kapcsolódás a: {node} 🎉",
"1nYUGC": "{n} Követek",
"1o2BgB": "Aláírások ellenörzése",
@ -298,6 +299,7 @@
"Xopqkl": "Az alapértelmezett zap összeg {number} sats, a példaértékek kiszámítása ebből történik.",
"XrSk2j": "Beváltás",
"YDURw6": "Szervíz cím",
"YR2I9M": "Nincsenek kulcsok, nincs {app}, Nincs mód a visszaállításra, ha nem készítesz biztonsági mentést. Csak egy percet vesz igénybe.",
"YXA3AH": "Reakciók engedélyezése",
"Z4BMCZ": "Párosító kifejezés megadása",
"ZKORll": "Aktiválás",
@ -381,6 +383,7 @@
"ieGrWo": "Követem",
"itPgxd": "Profil",
"izWS4J": "Követés visszavonása",
"j9xbzF": "Már kész a biztonsági mentés",
"jA3OE/": "{n,plural,one {}=1{{n} sat} other{{n} sats}}",
"jAmfGl": "{site_name}-előfizetése lejárt",
"jHa/ko": "Tisztítsa meg a takarmányt",
@ -442,6 +445,7 @@
"qz9fty": "Helytelen Pin-kód",
"r3C4x/": "Szoftver",
"r5srDR": "Add meg a pénztárcád jelszavát",
"rMgF34": "Biztonsági mentés",
"rT14Ow": "Csomópont hozzáadása",
"rbrahO": "Bezárás",
"rfuMjE": "(Alapértelmezett)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Apa Anda yakin Anda ingin memindahkan catatan ini dari penanda buku?",
"1R43+L": "Masukkan konfigurasi Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Terhubung ke: {node} 🎉",
"1nYUGC": "{n} Mengikuti",
"1o2BgB": "Periksa Tanda Tangan",
@ -298,6 +299,7 @@
"Xopqkl": "Jumlah zap default Anda adalah {number} sats, nilai contoh dihitung dari ini.",
"XrSk2j": "Tebus",
"YDURw6": "URL layanan",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Aktifkan reaksi",
"Z4BMCZ": "Masukkan frasa pasangan",
"ZKORll": "Aktifkan Sekarang",
@ -381,6 +383,7 @@
"ieGrWo": "Ikuti",
"itPgxd": "Profil",
"izWS4J": "Berhenti mengikuti",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sat}}",
"jAmfGl": "Langganan {site_name} Anda telah kedaluwarsa",
"jHa/ko": "Bersihkan feed Anda",
@ -442,6 +445,7 @@
"qz9fty": "Pin salah",
"r3C4x/": "Perangkat lunak",
"r5srDR": "Masukkan kata sandi dompet",
"rMgF34": "Back up now",
"rT14Ow": "Tambahkan Relay",
"rbrahO": "Tutup",
"rfuMjE": "(Bawaan)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Vuoi davvero rimuovere questa nota dai preferiti?",
"1R43+L": "Inserire la configurazione di Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Connessione a: {node}🎉",
"1nYUGC": "{n} seguiti",
"1o2BgB": "Firma degli assegni",
@ -298,6 +299,7 @@
"Xopqkl": "La quantità di zap predefinita è {number} sats, i valori di esempio sono calcolati da questo valore.",
"XrSk2j": "Riscatto",
"YDURw6": "URL del servizio",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Abilita reazioni",
"Z4BMCZ": "Inserisci la frase di accoppiamento",
"ZKORll": "Attiva adesso",
@ -381,6 +383,7 @@
"ieGrWo": "Segui",
"itPgxd": "Profilo",
"izWS4J": "Smetti di seguire",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "L'abbonamento a {site_name} è scaduto",
"jHa/ko": "Pulite il vostro feed",
@ -442,6 +445,7 @@
"qz9fty": "Pin non corretto",
"r3C4x/": "Software",
"r5srDR": "Inserire la password del portafogli",
"rMgF34": "Back up now",
"rT14Ow": "Aggiungi Relays",
"rbrahO": "Chiudere",
"rfuMjE": "(Predefinito)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "本当にこの投稿をブックマークから削除しますか?",
"1R43+L": "Nostr Wallet Connectの設定値を入力",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "{node}に接続しました🎉",
"1nYUGC": "{n} フォロー",
"1o2BgB": "小切手署名",
@ -298,6 +299,7 @@
"Xopqkl": "デフォルトのザップ量は{number} satsで、例示された値はここから算出されます。",
"XrSk2j": "交換",
"YDURw6": "サービスURL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "リアクションを有効にする",
"Z4BMCZ": "ペアリングフレーズを入力",
"ZKORll": "今すぐ有効化",
@ -381,6 +383,7 @@
"ieGrWo": "フォロー",
"itPgxd": "プロフィール",
"izWS4J": "フォロー解除",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "{site_name} 、有効期限が切れています。",
"jHa/ko": "フィードをきれいにする",
@ -442,6 +445,7 @@
"qz9fty": "ピンが正しくない",
"r3C4x/": "ソフトウェア",
"r5srDR": "ウォレットのパスワードを入力",
"rMgF34": "Back up now",
"rT14Ow": "リレーを追加する",
"rbrahO": "閉じる",
"rfuMjE": "(デフォルト)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Weet u zeker dat u deze notitie uit bladwijzers wilt verwijderen?",
"1R43+L": "Voer Nostr Wallet Connect configuratie in",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Verbonden met: {node}🎉",
"1nYUGC": "{n} Volgend",
"1o2BgB": "Handtekeningen controleren",
@ -298,6 +299,7 @@
"Xopqkl": "Uw standaard zap bedrag is {number} sats, voorbeeldwaarden worden hiermee berekend.",
"XrSk2j": "Inwisselen",
"YDURw6": "Service-URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Reacties inschakelen",
"Z4BMCZ": "Voer koppelingszin in",
"ZKORll": "Activeer Nu",
@ -381,6 +383,7 @@
"ieGrWo": "Volgen",
"itPgxd": "Profiel",
"izWS4J": "Ontvolgen",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,one {}=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Uw {site_name} abonnement is verlopen",
"jHa/ko": "Ruim je feed op",
@ -442,6 +445,7 @@
"qz9fty": "Verkeerde pin",
"r3C4x/": "Software",
"r5srDR": "Voer wallet wachtwoord in",
"rMgF34": "Back up now",
"rT14Ow": "Relays toevoegen",
"rbrahO": "Sluit",
"rfuMjE": "(Standaard)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Tem certeza que deseja remover esta nota dos favoritos?",
"1R43+L": "Insira a configuração da Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Conectado em: {node} 🎉",
"1nYUGC": "{n} Seguindo",
"1o2BgB": "Assinaturas de cheques",
@ -298,6 +299,7 @@
"Xopqkl": "Sua quantidade padrão de zap é de {number} sats, valores de exemplo são calculados a partir disso.",
"XrSk2j": "Resgatar",
"YDURw6": "URL do serviço",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Habilitar reações",
"Z4BMCZ": "Inserir frase de pareamento",
"ZKORll": "Ativar agora",
@ -381,6 +383,7 @@
"ieGrWo": "Seguir",
"itPgxd": "Perfil",
"izWS4J": "Deixar de seguir",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Sua assinatura do {site_name} expirou",
"jHa/ko": "Limpe seu feed",
@ -442,6 +445,7 @@
"qz9fty": "Pino incorreto",
"r3C4x/": "Software",
"r5srDR": "Digite a senha da carteira",
"rMgF34": "Back up now",
"rT14Ow": "Adicionar Relés",
"rbrahO": "Fechar",
"rfuMjE": "(Padrão)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Вы уверены, что хотите удалить эту заметку из закладок?",
"1R43+L": "Введите конфигурацию Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Подключен к: {node} 🎉",
"1nYUGC": "{n} Подписчиков",
"1o2BgB": "Контрольные подписи",
@ -298,6 +299,7 @@
"Xopqkl": "По умолчанию величина zap равна {number} sats, примерные значения рассчитываются исходя из этого.",
"XrSk2j": "Получить",
"YDURw6": "URL-адрес сервиса",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Включить реакции",
"Z4BMCZ": "Введи фразу для сопряжения",
"ZKORll": "Активировать",
@ -381,6 +383,7 @@
"ieGrWo": "Подписаться",
"itPgxd": "Профиль",
"izWS4J": "Отписаться",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Срок действия вашей подписки {site_name} истек",
"jHa/ko": "Очистка корма",
@ -442,6 +445,7 @@
"qz9fty": "Неправильный вывод",
"r3C4x/": "Программное обеспечение",
"r5srDR": "Введите пароль кошелька",
"rMgF34": "Back up now",
"rT14Ow": "Добавить реле",
"rbrahO": "Закрыть",
"rfuMjE": "(по умолчанию)",

View File

@ -21,15 +21,16 @@
"08zn6O": "Exportera nycklar",
"0Azlrb": "Hantera",
"0BUTMv": "Sök...",
"0HFX0T": "Use Exact Location",
"0HFX0T": "Använd exakt plats",
"0jOEtS": "Ogiltig LNURL",
"0mch2Y": "namnet har otillåtna tecken",
"0siT4z": "Politik",
"0uoY11": "Visa status",
"0yO7wF": "{n} secs",
"1H4Keq": "{n} users",
"1H4Keq": "{n} användare",
"1Mo59U": "Är du säker på att du vill ta bort den här anteckningen från bokmärken?",
"1R43+L": "Skriv in Nostr Wallet Connect konfiguration",
"1UWegE": "Se till att säkerhetskopiera dina nycklar!",
"1c4YST": "Ansluten till: {node}🎉",
"1nYUGC": "{n} Följer",
"1o2BgB": "Kontrollera signaturer",
@ -100,7 +101,7 @@
"9wO4wJ": "Lightning-faktura",
"ABAQyo": "Chattar",
"ADmfQT": "Förälder",
"AIgmDy": "Add up to 4 hashtags",
"AIgmDy": "Lägg till upp till 4 hashtaggar",
"AN0Z7Q": "Tystade ord",
"ASRK0S": "Denna författare har tystats",
"Ai8VHU": "Obegränsat antal anteckningar på Snort-relä",
@ -124,7 +125,7 @@
"CmZ9ls": "{n} tystad",
"CsCUYo": "{n} sats",
"Cu/K85": "Översatt från {lang}",
"CzHZoc": "Social Graph",
"CzHZoc": "Social graf",
"D+KzKd": "Zappa automatiskt varje anteckning när den är laddad",
"D3idYv": "Inställningar",
"DBiVK1": "Cache",
@ -276,7 +277,7 @@
"Ub+AGc": "Logga in",
"Up5U7K": "Blockera",
"UrKTqQ": "Du har ett aktivt iris.to konto",
"VL900k": "Recommended Relays",
"VL900k": "Rekommenderade reläer",
"VN0+Fz": "Saldo: {amount} sats",
"VOjC1i": "Välj vilken uppladdningstjänst du vill ladda upp bilagor till",
"VR5eHw": "Publik nyckel (npub/nprofile)",
@ -293,11 +294,12 @@
"X7xU8J": "nsec, npub, nip-05, hex, mnemonic",
"XECMfW": "Skicka användningsstatistik",
"XICsE8": "Filvärdar",
"XXm7jJ": "Trending Hashtags",
"XXm7jJ": "Trendande hashtaggar",
"XgWvGA": "Reaktioner",
"Xopqkl": "Ditt förvalda zap-belopp är {number} sats, exempelvärden beräknas utifrån detta.",
"XrSk2j": "Lös in",
"YDURw6": "Service URL",
"YR2I9M": "Inga nycklar, ingen {app}, Det finns inget sätt att återställa den om du inte säkerhetskopierar. Det tar bara en minut.",
"YXA3AH": "Aktivera reaktioner",
"Z4BMCZ": "Ange parningsfras",
"ZKORll": "Aktivera nu",
@ -331,9 +333,9 @@
"d+6YsV": "Listor för att stänga av ljudet:",
"d6CyG5": "Historik",
"d7d0/x": "LN Adress",
"d8gpCh": "Try to use less than 5 hashtags to stay on topic 🙏",
"d8gpCh": "Försök att använda färre än 5 hashtags för att hålla dig till ämnet 🙏",
"dOQCL8": "Visnings namn",
"ddd3JX": "Popular Hashtags",
"ddd3JX": "Populära Hashtaggar",
"deEeEI": "Registrering",
"dmsiLv": "En standard Zap Pool split av {n} har konfigurerats för {site} utvecklare, du kan inaktivera den när som helst i {link}",
"e61Jf3": "Kommer snart",
@ -344,7 +346,7 @@
"eJj8HD": "Bli Verifierad",
"eSzf2G": "En enda zap med {nIn} sats kommer att fördela {nOut} sats till zappoolen.",
"eXT2QQ": "Gruppchatt",
"egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}",
"egib+2": "{n,plural,=1{& {n} andra} other{& {n} andras}}",
"fBI91o": "Zap",
"fBlba3": "Tack för att du använder {site}, vänligen överväg att donera om du kan.",
"fOksnD": "Kan inte rösta eftersom LNURL-tjänsten inte stöder zaps",
@ -381,6 +383,7 @@
"ieGrWo": "Följ",
"itPgxd": "Profil",
"izWS4J": "Sluta följ",
"j9xbzF": "Redan säkerhetskopierad",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Ditt {site_name} abonnemang har löpt ut",
"jHa/ko": "Städa upp i ditt flöde",
@ -442,6 +445,7 @@
"qz9fty": "Felaktig pin",
"r3C4x/": "Mjukvara",
"r5srDR": "Ange lösenord för plånboken",
"rMgF34": "Säkerhetskopiera nu",
"rT14Ow": "Lägg till reläer",
"rbrahO": "Stäng",
"rfuMjE": "(Standard)",
@ -464,7 +468,7 @@
"uSV4Ti": "Dela vidare måste bekräftas manuellt",
"uc0din": "Skicka sats-delningar till",
"ugyJnE": "Skicka anteckningar och andra saker",
"un1nGw": "{n} notes",
"un1nGw": "{n} anteckningar",
"usAvMr": "Redigera profil",
"v8lolG": "Starta chatt",
"vB3oQ/": "Måste vara en kontaktlista eller en pubkey lista",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "Je, una uhakika unataka kuondoa dokezo hili kutoka kwa vialamisho?",
"1R43+L": "Ingiza usanidi wa Nostr Wallet Connect",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Imeunganishwa kwa: {node} 🎉",
"1nYUGC": "{n} Unafuata",
"1o2BgB": "Check Signatures",
@ -298,6 +299,7 @@
"Xopqkl": "Kiasi chako chaguomsingi cha zap ni {number} sats, thamani za mfano zinakokotolewa kutoka hii.",
"XrSk2j": "Komboa",
"YDURw6": "URL ya huduma",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "Washa maitikio",
"Z4BMCZ": "Weka maneno ya kuoanisha",
"ZKORll": "Washa Sasa",
@ -381,6 +383,7 @@
"ieGrWo": "Fuata",
"itPgxd": "Wasifu",
"izWS4J": "Acha kufuata",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,one {}=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -442,6 +445,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "Programu",
"r5srDR": "Ingiza nenosiri la pochi",
"rMgF34": "Back up now",
"rT14Ow": "Ongeza Relay",
"rbrahO": "Close",
"rfuMjE": "(Chaguo-msingi)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "இந்தக் குறிப்பைப் புக்மார்க்குகளிலிருந்து அகற்ற நிச்சயமாக விரும்புகிறீர்களா?",
"1R43+L": "நாஸ்டர் பணப்பை இணைப்புக் கட்டமைப்பை உள்ளிடவும்",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "{node} உடன் இணைக்கப் பட்டது 🎉",
"1nYUGC": "{n} பின்தொடரப் படுவோர்",
"1o2BgB": "கையெழுத்துக்களை சரிபார்",
@ -298,6 +299,7 @@
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
"XrSk2j": "Redeem",
"YDURw6": "சேவை URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "எதிர்வினைகளை அனுமதி",
"Z4BMCZ": "இணைத்தல் சொற்றொடரை உள்ளிடவும்",
"ZKORll": "இப்போது செயல்படுத்துக",
@ -381,6 +383,7 @@
"ieGrWo": "பின்தொடர்",
"itPgxd": "சுயவிவரம்",
"izWS4J": "பின்தொடர்வதை நிறுத்துக",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} ஸாட்} other{{n} ஸாட்கள்}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -442,6 +445,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "மென்பொருள்",
"r5srDR": "பணப்பையின் கடவுச்சொல்லை உள்ளிடவும்",
"rMgF34": "Back up now",
"rT14Ow": "ரிலேகளைச் சேர்க்கவும்",
"rbrahO": "Close",
"rfuMjE": "(இயல்புநிலை)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "คุณแน่ใจหรือว่าต้องการลบโน้ตนี้ออกจากบุ๊คมาร์ค?",
"1R43+L": "ใส่ Nostr Wallet Connect config",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "เชื่อมต่อกับ: {node} 🎉",
"1nYUGC": "กำลังติดตาม {n}",
"1o2BgB": "Check Signatures",
@ -298,6 +299,7 @@
"Xopqkl": "จํานวน zap เริ่มต้นของคุณคือ {number} sats ค่าตัวอย่างคํานวณจากสิ่งนี้",
"XrSk2j": "รับคืน",
"YDURw6": "Service URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "เปิดใช้งาน reactions",
"Z4BMCZ": "ใส่ pairing phrase",
"ZKORll": "เปิดใช้งานทันที",
@ -381,6 +383,7 @@
"ieGrWo": "ติดตาม",
"itPgxd": "โปรไฟล์",
"izWS4J": "เลิกติดตาม",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} sat} other{{n} sats}}",
"jAmfGl": "Your {site_name} subscription is expired",
"jHa/ko": "Clean up your feed",
@ -442,6 +445,7 @@
"qz9fty": "Incorrect pin",
"r3C4x/": "ซอฟต์แวร์",
"r5srDR": "โปรดใส่รหัส wallet",
"rMgF34": "Back up now",
"rT14Ow": "เพิ่มรีเลย์",
"rbrahO": "ปิด",
"rfuMjE": "(ค่าเริ่มต้น)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "是否确定要从收藏中移除此条笔记?",
"1R43+L": "输入 Nostr Wallet Connect 配置",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "已连接到:{node}🎉",
"1nYUGC": "{n} 个关注",
"1o2BgB": "检查签名",
@ -298,6 +299,7 @@
"Xopqkl": "你的默认打闪金额是 {number} 聪,示例值是以此计算的。",
"XrSk2j": "兑现",
"YDURw6": "服务网址",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "启用回应",
"Z4BMCZ": "输入配对词句",
"ZKORll": "立即激活",
@ -381,6 +383,7 @@
"ieGrWo": "关注",
"itPgxd": "个人档案",
"izWS4J": "取消关注",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n}聪} other{{n}聪}}",
"jAmfGl": "你的 {site_name} 订阅已过期",
"jHa/ko": "清理你的订阅",
@ -442,6 +445,7 @@
"qz9fty": "PIN 码不正确",
"r3C4x/": "软件",
"r5srDR": "输入钱包密码",
"rMgF34": "Back up now",
"rT14Ow": "添加中继",
"rbrahO": "关闭",
"rfuMjE": "(默认)",

View File

@ -30,6 +30,7 @@
"1H4Keq": "{n} users",
"1Mo59U": "是否確定要從收藏中移除此條筆記?",
"1R43+L": "輸入 Nostr Wallet Connect 配置",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "已連接到:{node} 🎉",
"1nYUGC": "{n} 個關注",
"1o2BgB": "檢查簽名",
@ -298,6 +299,7 @@
"Xopqkl": "你的默認打閃金額是 {number} 聰,示例值是以此計算的。",
"XrSk2j": "兌現",
"YDURw6": "服務 URL",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YXA3AH": "启用回應",
"Z4BMCZ": "輸入配對詞句",
"ZKORll": "立即激活",
@ -381,6 +383,7 @@
"ieGrWo": "關注",
"itPgxd": "個人檔案",
"izWS4J": "取消關注",
"j9xbzF": "Already backed up",
"jA3OE/": "{n,plural,=1{{n} 聰} other{{n} 聰}}",
"jAmfGl": "你的 {site_name} 訂閱已過期了",
"jHa/ko": "清理你的訂閱",
@ -442,6 +445,7 @@
"qz9fty": "PIN 碼不正確",
"r3C4x/": "軟件",
"r5srDR": "輸入錢包密碼",
"rMgF34": "Back up now",
"rT14Ow": "添加中繼器",
"rbrahO": "關閉",
"rfuMjE": "(默認)",

View File

@ -3,7 +3,6 @@ import { VitePWA } from "vite-plugin-pwa";
import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig } from "vite";
import { vitePluginVersionMark } from "vite-plugin-version-mark";
import appConfig from "config";
export default defineConfig({
@ -33,7 +32,7 @@ export default defineConfig({
build: {
outDir: "build",
},
base: "",
clearScreen: false,
publicDir: appConfig.get("publicDir"),
resolve: {
alias: {