solve conflicts
This commit is contained in:
commit
5d137f281f
@ -4,9 +4,9 @@
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app build",
|
||||
"start": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app start",
|
||||
"test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test",
|
||||
"build": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-web build && yarn workspace @snort/system-react build && yarn workspace @snort/app build",
|
||||
"start": "yarn build && yarn workspace @snort/app start",
|
||||
"test": "yarn build && yarn workspace @snort/app test && yarn workspace @snort/system test",
|
||||
"pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ."
|
||||
},
|
||||
"prettier": {
|
||||
|
@ -6,5 +6,8 @@
|
||||
"favicon": "public/favicon.ico",
|
||||
"appleTouchIconUrl": "/nostrich_512.png",
|
||||
"httpCache": "",
|
||||
"animalNamePlaceholders": false
|
||||
"animalNamePlaceholders": false,
|
||||
"features": {
|
||||
"subscriptions": true
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,8 @@
|
||||
"favicon": "public/iris/favicon.ico",
|
||||
"appleTouchIconUrl": "/img/apple-touch-icon.png",
|
||||
"httpCache": "https://api.iris.to",
|
||||
"animalNamePlaceholders": true
|
||||
"animalNamePlaceholders": true,
|
||||
"features": {
|
||||
"subscriptions": false
|
||||
}
|
||||
}
|
||||
|
14
packages/app/custom.d.ts
vendored
14
packages/app/custom.d.ts
vendored
@ -34,3 +34,17 @@ declare module "emojilib" {
|
||||
const value: Record<string, string>;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare const CONFIG: {
|
||||
appName: string;
|
||||
appNameCapitalized: string;
|
||||
appTitle: string;
|
||||
nip05Domain: string;
|
||||
favicon: string;
|
||||
appleTouchIconUrl: string;
|
||||
httpCache: string;
|
||||
animalNamePlaceholders: boolean;
|
||||
features: {
|
||||
subscriptions: boolean;
|
||||
};
|
||||
};
|
||||
|
@ -13,8 +13,10 @@
|
||||
"@snort/system": "workspace:*",
|
||||
"@snort/system-react": "workspace:*",
|
||||
"@snort/system-wasm": "workspace:*",
|
||||
"@snort/system-web": "workspace:*",
|
||||
"@szhsin/react-menu": "^3.3.1",
|
||||
"@types/use-sync-external-store": "^0.0.4",
|
||||
"@uidotdev/usehooks": "^2.3.1",
|
||||
"@void-cat/api": "^1.0.4",
|
||||
"debug": "^4.3.4",
|
||||
"dexie": "^3.2.4",
|
||||
@ -30,6 +32,7 @@
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"react-twitter-embed": "^4.0.4",
|
||||
"recharts": "^2.8.0",
|
||||
"use-long-press": "^3.2.0",
|
||||
"use-sync-external-store": "^1.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
|
@ -342,6 +342,11 @@
|
||||
<symbol id="shield-tick" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 11.5L11 13.5L15.5 8.99999M20 12C20 16.9084 14.646 20.4784 12.698 21.6149C12.4766 21.744 12.3659 21.8086 12.2097 21.8421C12.0884 21.8681 11.9116 21.8681 11.7903 21.8421C11.6341 21.8086 11.5234 21.744 11.302 21.6149C9.35396 20.4784 4 16.9084 4 12V7.21759C4 6.41808 4 6.01833 4.13076 5.6747C4.24627 5.37113 4.43398 5.10027 4.67766 4.88552C4.9535 4.64243 5.3278 4.50207 6.0764 4.22134L11.4382 2.21067C11.6461 2.13271 11.75 2.09373 11.857 2.07827C11.9518 2.06457 12.0482 2.06457 12.143 2.07827C12.25 2.09373 12.3539 2.13271 12.5618 2.21067L17.9236 4.22134C18.6722 4.50207 19.0465 4.64243 19.3223 4.88552C19.566 5.10027 19.7537 5.37113 19.8692 5.6747C20 6.01833 20 6.41808 20 7.21759V12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol id="at-sign" viewBox="0 0 21 20" fill="none">
|
||||
<g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0453 1.07385C11.0306 0.603671 8.91602 0.82887 7.04549 1.71284C5.17495 2.5968 3.65845 4.08754 2.74257 5.94266C1.82669 7.79778 1.5653 9.90817 2.00091 11.9307C2.43651 13.9532 3.54348 15.7689 5.14183 17.0825C6.74018 18.3961 8.7359 19.1304 10.8045 19.166C12.8731 19.2015 14.8929 18.5363 16.5354 17.2784C16.9008 16.9986 16.9702 16.4755 16.6904 16.1101C16.4105 15.7447 15.8875 15.6754 15.5221 15.9552C14.1782 16.9844 12.5256 17.5287 10.8331 17.4995C9.14066 17.4704 7.5078 16.8697 6.20006 15.7949C4.89232 14.7201 3.98661 13.2346 3.63021 11.5798C3.27381 9.92499 3.48767 8.1983 4.23703 6.68048C4.98638 5.16265 6.22716 3.94296 7.7576 3.21971C9.28804 2.49647 11.0181 2.31221 12.6666 2.69691C14.315 3.08161 15.7848 4.01263 16.837 5.33858C17.8892 6.66453 18.462 8.30748 18.4621 10.0002V10.8335C18.4621 11.2755 18.2865 11.6994 17.9739 12.012C17.6614 12.3245 17.2374 12.5001 16.7954 12.5001C16.3534 12.5001 15.9295 12.3245 15.6169 12.012C15.3043 11.6994 15.1288 11.2755 15.1288 10.8335V6.6668C15.1288 6.20656 14.7557 5.83347 14.2954 5.83347C13.8353 5.83347 13.4622 6.2064 13.4621 6.66651C12.7657 6.14343 11.9001 5.83348 10.9621 5.83348C8.6609 5.83348 6.79542 7.69896 6.79542 10.0001C6.79542 12.3013 8.6609 14.1668 10.9621 14.1668C12.2022 14.1668 13.3157 13.6251 14.079 12.7654C14.186 12.9159 14.3061 13.0582 14.4384 13.1905C15.0635 13.8156 15.9114 14.1668 16.7954 14.1668C17.6795 14.1668 18.5273 13.8156 19.1524 13.1905C19.7776 12.5654 20.1288 11.7175 20.1288 10.8335V10.0001C20.1286 7.93119 19.4286 5.92319 18.1426 4.30257C16.8565 2.68195 15.0601 1.54404 13.0453 1.07385ZM13.4621 9.99665V10.0036C13.4602 11.3827 12.3416 12.5001 10.9621 12.5001C9.58138 12.5001 8.46209 11.3809 8.46209 10.0001C8.46209 8.61943 9.58138 7.50014 10.9621 7.50014C12.3416 7.50014 13.4602 8.61754 13.4621 9.99665Z" fill="currentColor"/>
|
||||
</g>
|
||||
</symbol>
|
||||
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 93 KiB |
@ -24,7 +24,7 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
|
||||
override async onEvent(evs: Array<TaggedNostrEvent>, pub?: EventPublisher) {
|
||||
override async onEvent(evs: Array<TaggedNostrEvent>, _: string, pub?: EventPublisher) {
|
||||
if (!pub) return;
|
||||
|
||||
const unwrapped = (
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { EventKind, NostrEvent, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache";
|
||||
import { LoginSession } from "Login";
|
||||
import { db } from "Db";
|
||||
import { NostrEventForSession, db } from "Db";
|
||||
import { Day } from "Const";
|
||||
import { unixNow } from "@snort/shared";
|
||||
|
||||
export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
|
||||
export class NotificationsCache extends RefreshFeedCache<NostrEventForSession> {
|
||||
#kinds = [EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt];
|
||||
|
||||
constructor() {
|
||||
@ -14,7 +14,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
|
||||
|
||||
buildSub(session: LoginSession, rb: RequestBuilder) {
|
||||
if (session.publicKey) {
|
||||
const newest = this.newest();
|
||||
const newest = this.newest(v => v.tags.some(a => a[0] === "p" && a[1] === session.publicKey));
|
||||
rb.withFilter()
|
||||
.kinds(this.#kinds)
|
||||
.tag("p", [session.publicKey])
|
||||
@ -22,10 +22,15 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
async onEvent(evs: readonly TaggedNostrEvent[]) {
|
||||
async onEvent(evs: readonly TaggedNostrEvent[], pubKey: string) {
|
||||
const filtered = evs.filter(a => this.#kinds.includes(a.kind) && a.tags.some(b => b[0] === "p"));
|
||||
if (filtered.length > 0) {
|
||||
await this.bulkSet(filtered);
|
||||
await this.bulkSet(
|
||||
filtered.map(v => ({
|
||||
...v,
|
||||
forSession: pubKey,
|
||||
})),
|
||||
);
|
||||
this.notifyChange(filtered.map(v => this.key(v)));
|
||||
}
|
||||
}
|
||||
@ -34,7 +39,7 @@ export class NotificationsCache extends RefreshFeedCache<NostrEvent> {
|
||||
return of.id;
|
||||
}
|
||||
|
||||
takeSnapshot(): TWithCreated<NostrEvent>[] {
|
||||
takeSnapshot() {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,18 @@ export type TWithCreated<T> = (T | Readonly<T>) & { created_at: number };
|
||||
|
||||
export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> {
|
||||
abstract buildSub(session: LoginSession, rb: RequestBuilder): void;
|
||||
abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pub?: EventPublisher): void;
|
||||
abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pubKey: string, pub?: EventPublisher): void;
|
||||
|
||||
/**
|
||||
* Get latest event
|
||||
*/
|
||||
protected newest() {
|
||||
protected newest(filter?: (e: TWithCreated<T>) => boolean) {
|
||||
let ret = 0;
|
||||
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
|
||||
this.cache.forEach(v => {
|
||||
if (!filter || filter(v)) {
|
||||
ret = v.created_at > ret ? v.created_at : ret;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/system";
|
||||
import { SnortSystemDb } from "@snort/system-web";
|
||||
|
||||
import { EventInteractionCache } from "./EventInteractionCache";
|
||||
import { ChatCache } from "./ChatCache";
|
||||
import { Payments } from "./PaymentsCache";
|
||||
@ -6,9 +8,11 @@ import { GiftWrapCache } from "./GiftWrapCache";
|
||||
import { NotificationsCache } from "./Notifications";
|
||||
import { FollowsFeedCache } from "./FollowsFeed";
|
||||
|
||||
export const UserCache = new UserProfileCache();
|
||||
export const UserRelays = new UserRelaysCache();
|
||||
export const RelayMetrics = new RelayMetricCache();
|
||||
export const SystemDb = new SnortSystemDb();
|
||||
export const UserCache = new UserProfileCache(SystemDb.users);
|
||||
export const UserRelays = new UserRelaysCache(SystemDb.userRelays);
|
||||
export const RelayMetrics = new RelayMetricCache(SystemDb.relayMetrics);
|
||||
|
||||
export const Chats = new ChatCache();
|
||||
export const PaymentsCache = new Payments();
|
||||
export const InteractionCache = new EventInteractionCache();
|
||||
|
@ -2,7 +2,7 @@ import Dexie, { Table } from "dexie";
|
||||
import { HexKey, NostrEvent, TaggedNostrEvent, u256 } from "@snort/system";
|
||||
|
||||
export const NAME = "snortDB";
|
||||
export const VERSION = 14;
|
||||
export const VERSION = 15;
|
||||
|
||||
export interface SubCache {
|
||||
id: string;
|
||||
@ -35,6 +35,10 @@ export interface UnwrappedGift {
|
||||
tags?: Array<Array<string>>; // some tags extracted
|
||||
}
|
||||
|
||||
export type NostrEventForSession = TaggedNostrEvent & {
|
||||
forSession: string;
|
||||
};
|
||||
|
||||
const STORES = {
|
||||
chats: "++id",
|
||||
eventInteraction: "++id",
|
||||
@ -50,7 +54,7 @@ export class SnortDB extends Dexie {
|
||||
eventInteraction!: Table<EventInteraction>;
|
||||
payments!: Table<Payment>;
|
||||
gifts!: Table<UnwrappedGift>;
|
||||
notifications!: Table<NostrEvent>;
|
||||
notifications!: Table<NostrEventForSession>;
|
||||
followsFeed!: Table<TaggedNostrEvent>;
|
||||
|
||||
constructor() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
.reactions-modal .modal-body {
|
||||
padding: 24px 32px;
|
||||
background-color: #1b1b1b;
|
||||
background-color: var(--gray-superdark);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
min-height: 33vh;
|
||||
|
@ -205,9 +205,10 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
|
||||
);
|
||||
};
|
||||
|
||||
export function ThreadRoute() {
|
||||
export function ThreadRoute({ id }: { id?: string }) {
|
||||
const params = useParams();
|
||||
const link = parseNostrLink(params.id ?? "", NostrPrefix.Note);
|
||||
const resolvedId = id ?? params.id;
|
||||
const link = parseNostrLink(resolvedId ?? "", NostrPrefix.Note);
|
||||
|
||||
return (
|
||||
<ThreadContextWrapper link={link}>
|
||||
|
@ -12,7 +12,7 @@ const ExtendedFormattedMessage: FC<ExtendedProps> = props => {
|
||||
useEffect(() => {
|
||||
const translatedMessage = formatMessage({ id, defaultMessage }, values);
|
||||
if (typeof translatedMessage === "string") {
|
||||
setProcessedMessage(translatedMessage.replace("Snort", process.env.APP_NAME_CAPITALIZED || "Snort"));
|
||||
setProcessedMessage(translatedMessage.replace("Snort", CONFIG.appNameCapitalized || "Snort"));
|
||||
}
|
||||
}, [id, defaultMessage, values, formatMessage]);
|
||||
|
||||
|
@ -4,7 +4,7 @@ const Logo = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<h1 className="logo" onClick={() => navigate("/")}>
|
||||
{process.env.APP_NAME}
|
||||
{CONFIG.appNameCapitalized}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
|
@ -145,7 +145,7 @@ export function LoginUnlock() {
|
||||
<FormattedMessage
|
||||
defaultMessage="Enter a pin to encrypt your private key, you must enter this pin every time you open {site}."
|
||||
values={{
|
||||
site: process.env.APP_NAME_CAPITALIZED,
|
||||
site: CONFIG.appNameCapitalized,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
@ -34,7 +34,7 @@
|
||||
border-radius: 100px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
|
@ -31,7 +31,7 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
return (
|
||||
<div className="tabs p" ref={horizontalScroll}>
|
||||
<div className="tabs" ref={horizontalScroll}>
|
||||
{tabs.map(t => (
|
||||
<TabElement tab={tab} setTab={setTab} t={t} />
|
||||
))}
|
||||
|
@ -113,3 +113,10 @@
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gallery:not(:first-child),
|
||||
img:not(:first-child),
|
||||
video:not(:first-child),
|
||||
.link-preview-container:not(:first-child) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
@ -184,6 +184,8 @@ export default function Text({
|
||||
if (nextElement && nextElement.type === "media" && nextElement.mimeType?.startsWith("image")) {
|
||||
galleryImages.push(nextElement);
|
||||
i++;
|
||||
} else if (nextElement && nextElement.type === "text" && nextElement.content.trim().length === 0) {
|
||||
i++; //skip over empty space text
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -248,10 +250,14 @@ export default function Text({
|
||||
chunks.push(<CashuNuts token={element.content} />);
|
||||
}
|
||||
if (element.type === "link" || (element.type === "media" && element.mimeType?.startsWith("unknown"))) {
|
||||
if (disableMedia ?? false) {
|
||||
chunks.push(<DisableMedia content={element.content} />);
|
||||
} else {
|
||||
chunks.push(
|
||||
<HyperText link={element.content} depth={depth} showLinkPreview={!(disableLinkPreview ?? false)} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (element.type === "custom_emoji") {
|
||||
chunks.push(<ProxyImg src={element.content} size={15} className="custom-emoji" />);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "./DisplayName.css";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { HexKey, UserMetadata, NostrPrefix } from "@snort/system";
|
||||
import AnimalName from "Element/User/AnimalName";
|
||||
import { hexToBech32 } from "SnortUtils";
|
||||
@ -22,7 +22,7 @@ export function getDisplayNameOrPlaceHolder(user: UserMetadata | undefined, pubk
|
||||
name = user.display_name;
|
||||
} else if (typeof user?.name === "string" && user.name.length > 0) {
|
||||
name = user.name;
|
||||
} else if (pubkey && process.env.ANIMAL_NAME_PLACEHOLDERS) {
|
||||
} else if (pubkey && CONFIG.animalNamePlaceholders) {
|
||||
name = AnimalName(pubkey);
|
||||
isPlaceHolder = true;
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
.follow-button {
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import "./FollowButton.css";
|
||||
import FormattedMessage from "Element/FormattedMessage";
|
||||
import { HexKey } from "@snort/system";
|
||||
|
||||
@ -20,7 +19,7 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
const publisher = useEventPublisher();
|
||||
const { follows, relays, readonly } = useLogin(s => ({ follows: s.follows, relays: s.relays, readonly: s.readonly }));
|
||||
const isFollowing = follows.item.includes(pubkey);
|
||||
const baseClassname = `${props.className ? ` ${props.className}` : ""}follow-button`;
|
||||
const baseClassname = props.className ? `${props.className} ` : "";
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
if (publisher) {
|
||||
@ -42,9 +41,12 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
|
||||
return (
|
||||
<AsyncButton
|
||||
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
||||
className={isFollowing ? `${baseClassname} secondary` : `${baseClassname} primary`}
|
||||
disabled={readonly}
|
||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}>
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
isFollowing ? unfollow(pubkey) : follow(pubkey);
|
||||
}}>
|
||||
{isFollowing ? <FormattedMessage {...messages.Unfollow} /> : <FormattedMessage {...messages.Follow} />}
|
||||
</AsyncButton>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import "./Nip05.css";
|
||||
import { HexKey } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) {
|
||||
export function useIsVerified(pubkey?: HexKey, bypassCheck?: boolean) {
|
||||
const profile = useUserProfile(pubkey);
|
||||
return { isVerified: bypassCheck || profile?.isNostrAddressValid };
|
||||
}
|
||||
|
@ -43,3 +43,15 @@ a.pfp {
|
||||
background-color: var(--gray-superdark);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
width: 360px;
|
||||
border-radius: 16px;
|
||||
background: var(--gray-superdark);
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.profile-card > div {
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import "./ProfileImage.css";
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { HexKey, UserMetadata } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useHover } from "@uidotdev/usehooks";
|
||||
import { ControlledMenu } from "@szhsin/react-menu";
|
||||
|
||||
import { profileLink } from "SnortUtils";
|
||||
import Avatar from "Element/User/Avatar";
|
||||
@ -11,6 +13,9 @@ import Nip05 from "Element/User/Nip05";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import Icon from "Icons/Icon";
|
||||
import DisplayName from "./DisplayName";
|
||||
import Text from "Element/Text";
|
||||
import FollowButton from "Element/User/FollowButton";
|
||||
import { UserWebsiteLink } from "Element/User/UserWebsiteLink";
|
||||
|
||||
export interface ProfileImageProps {
|
||||
pubkey: HexKey;
|
||||
@ -27,6 +32,7 @@ export interface ProfileImageProps {
|
||||
imageOverlay?: ReactNode;
|
||||
showFollowingMark?: boolean;
|
||||
icons?: ReactNode;
|
||||
showProfileCard?: boolean;
|
||||
}
|
||||
|
||||
export default function ProfileImage({
|
||||
@ -44,11 +50,29 @@ export default function ProfileImage({
|
||||
onClick,
|
||||
showFollowingMark = true,
|
||||
icons,
|
||||
showProfileCard,
|
||||
}: ProfileImageProps) {
|
||||
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
||||
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
||||
const { follows } = useLogin();
|
||||
const doesFollow = follows.item.includes(pubkey);
|
||||
const [ref, hovering] = useHover<HTMLDivElement>();
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const [t, setT] = useState<ReturnType<typeof setTimeout>>();
|
||||
|
||||
useEffect(() => {
|
||||
if (hovering) {
|
||||
const tn = setTimeout(() => {
|
||||
setShowProfileMenu(true);
|
||||
}, 1000);
|
||||
setT(tn);
|
||||
} else {
|
||||
if (t) {
|
||||
clearTimeout(t);
|
||||
setT(undefined);
|
||||
}
|
||||
}
|
||||
}, [hovering]);
|
||||
|
||||
function handleClick(e: React.MouseEvent) {
|
||||
if (link === "") {
|
||||
@ -60,7 +84,7 @@ export default function ProfileImage({
|
||||
function inner() {
|
||||
return (
|
||||
<>
|
||||
<div className="avatar-wrapper">
|
||||
<div className="avatar-wrapper" ref={ref}>
|
||||
<Avatar
|
||||
pubkey={pubkey}
|
||||
user={user}
|
||||
@ -93,20 +117,63 @@ export default function ProfileImage({
|
||||
);
|
||||
}
|
||||
|
||||
function profileCard() {
|
||||
if (showProfileCard ?? true) {
|
||||
return (
|
||||
<ControlledMenu
|
||||
state={showProfileMenu ? "open" : "closed"}
|
||||
anchorRef={ref}
|
||||
menuClassName="profile-card"
|
||||
onClose={() => setShowProfileMenu(false)}>
|
||||
<div className="flex-column g8">
|
||||
<div className="flex f-space">
|
||||
<ProfileImage pubkey={""} profile={user} showProfileCard={false} link="" />
|
||||
<div className="flex g8">
|
||||
{/*<button type="button" onClick={() => {
|
||||
LoginStore.loginWithPubkey(pubkey, LoginSessionType.PublicKey, undefined, undefined, undefined, true);
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Stalk" />
|
||||
</button>*/}
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
<Text
|
||||
id={`profile-card-${pubkey}`}
|
||||
content={user?.about ?? ""}
|
||||
creator={pubkey}
|
||||
tags={[]}
|
||||
disableMedia={true}
|
||||
disableLinkPreview={true}
|
||||
truncate={250}
|
||||
/>
|
||||
<UserWebsiteLink user={user} />
|
||||
</div>
|
||||
</ControlledMenu>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (link === "") {
|
||||
return (
|
||||
<>
|
||||
<div className={`pfp${className ? ` ${className}` : ""}`} onClick={handleClick}>
|
||||
{inner()}
|
||||
</div>
|
||||
{profileCard()}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
className={`pfp${className ? ` ${className}` : ""}`}
|
||||
to={link === undefined ? profileLink(pubkey) : link}
|
||||
onClick={handleClick}>
|
||||
{inner()}
|
||||
</Link>
|
||||
{profileCard()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,3 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profile-preview button {
|
||||
min-width: 98px;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export interface ProfilePreviewProps {
|
||||
options?: {
|
||||
about?: boolean;
|
||||
linkToProfile?: boolean;
|
||||
profileCards?: boolean;
|
||||
};
|
||||
profile?: UserMetadata;
|
||||
actions?: ReactNode;
|
||||
@ -45,6 +46,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
profile={props.profile}
|
||||
link={options.linkToProfile ?? true ? undefined : ""}
|
||||
subHeader={options.about ? <div className="about">{user?.about}</div> : undefined}
|
||||
showProfileCard={options.profileCards}
|
||||
/>
|
||||
{props.actions ?? (
|
||||
<div className="follow-button-container">
|
||||
|
13
packages/app/src/Element/User/UserWebsiteLink.css
Normal file
13
packages/app/src/Element/User/UserWebsiteLink.css
Normal file
@ -0,0 +1,13 @@
|
||||
.user-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-profile-link a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-profile-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
29
packages/app/src/Element/User/UserWebsiteLink.tsx
Normal file
29
packages/app/src/Element/User/UserWebsiteLink.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import "./UserWebsiteLink.css";
|
||||
import { MetadataCache, UserMetadata } from "@snort/system";
|
||||
import Icon from "Icons/Icon";
|
||||
|
||||
export function UserWebsiteLink({ user }: { user?: MetadataCache | UserMetadata }) {
|
||||
const website_url =
|
||||
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
|
||||
|
||||
function tryFormatWebsite(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
if (user?.website) {
|
||||
return (
|
||||
<div className="user-profile-link f-ellipsis">
|
||||
<Icon name="link-02" size={16} />
|
||||
<a href={website_url} target="_blank" rel="noreferrer">
|
||||
{tryFormatWebsite(user.website)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ export default function useLoginFeed() {
|
||||
leaveOpen: true,
|
||||
});
|
||||
b.withFilter().authors([pubKey]).kinds([EventKind.ContactList]);
|
||||
if (!login.readonly) {
|
||||
if (CONFIG.features.subscriptions && !login.readonly) {
|
||||
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
|
||||
b.withFilter()
|
||||
.relay("wss://relay.snort.social")
|
||||
|
@ -5,6 +5,7 @@ import { NoopStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { RefreshFeedCache } from "Cache/RefreshFeedCache";
|
||||
import useLogin from "./useLogin";
|
||||
import useEventPublisher from "./useEventPublisher";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false) {
|
||||
const system = useContext(SnortContext);
|
||||
@ -33,7 +34,7 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
|
||||
tBuf = [...evs];
|
||||
t = setTimeout(() => {
|
||||
t = undefined;
|
||||
c.onEvent(tBuf, publisher);
|
||||
c.onEvent(tBuf, unwrap(login.publicKey), publisher);
|
||||
}, 100);
|
||||
} else {
|
||||
tBuf.push(...evs);
|
||||
@ -46,8 +47,5 @@ export function useRefreshFeedCache<T>(c: RefreshFeedCache<T>, leaveOpen = false
|
||||
releaseOnEvent();
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
// noop
|
||||
};
|
||||
}, [sub]);
|
||||
}
|
||||
|
@ -3,12 +3,15 @@ import { ParsedFragment, transformText } from "@snort/system";
|
||||
const TextCache = new Map<string, Array<ParsedFragment>>();
|
||||
|
||||
export function transformTextCached(id: string, content: string, tags: Array<Array<string>>) {
|
||||
if (content.length > 0) {
|
||||
const cached = TextCache.get(id);
|
||||
if (cached) return cached;
|
||||
const newCache = transformText(content, tags);
|
||||
TextCache.set(id, newCache);
|
||||
return newCache;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function useTextTransformer(id: string, content: string, tags: Array<Array<string>>) {
|
||||
return transformTextCached(id, content, tags);
|
||||
|
@ -128,4 +128,9 @@ export interface LoginSession {
|
||||
* A list of chats which we have joined (NIP-28/NIP-29)
|
||||
*/
|
||||
extraChats: Array<string>;
|
||||
|
||||
/**
|
||||
* Is login session in stalker mode
|
||||
*/
|
||||
stalker: boolean;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ const LoggedOut = {
|
||||
timestamp: 0,
|
||||
},
|
||||
extraChats: [],
|
||||
stalker: false,
|
||||
} as LoginSession;
|
||||
|
||||
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
@ -125,6 +126,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
relays?: Record<string, RelaySettings>,
|
||||
remoteSignerRelays?: Array<string>,
|
||||
privateKey?: KeyStorage,
|
||||
stalker?: boolean,
|
||||
) {
|
||||
if (this.#accounts.has(key)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
@ -143,6 +145,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
preferences: deepClone(DefaultPreferences),
|
||||
remoteSignerRelays,
|
||||
privateKeyData: privateKey,
|
||||
stalker: stalker ?? false,
|
||||
} as LoginSession;
|
||||
|
||||
const pub = createPublisher(newSession);
|
||||
|
@ -94,13 +94,13 @@ const DonatePage = () => {
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
defaultMessage="Help fund the development of {site}"
|
||||
values={{ site: process.env.APP_NAME_CAPITALIZED }}
|
||||
values={{ site: CONFIG.appNameCapitalized }}
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="{site} is an open source project built by passionate people in their free time"
|
||||
values={{ site: process.env.APP_NAME_CAPITALIZED }}
|
||||
values={{ site: CONFIG.appNameCapitalized }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
|
@ -90,3 +90,22 @@ header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stalker {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-shadow: 0px 0px 26px 0px rgba(139, 92, 246, 0.7) inset;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stalker button {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 50px;
|
||||
color: black;
|
||||
background-color: var(--btn-color);
|
||||
padding: 12px;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
@ -23,10 +23,12 @@ import { useLoginRelays } from "Hooks/useLoginRelays";
|
||||
import { useNoteCreator } from "State/NoteCreator";
|
||||
import { LoginUnlock } from "Element/PinPrompt";
|
||||
import useKeyboardShortcut from "Hooks/useKeyboardShortcut";
|
||||
import { LoginStore } from "Login";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
const [pageClass, setPageClass] = useState("page");
|
||||
const { id, stalker } = useLogin(s => ({ id: s.id, stalker: s.stalker ?? false }));
|
||||
|
||||
useLoginFeed();
|
||||
useTheme();
|
||||
@ -71,6 +73,17 @@ export default function Layout() {
|
||||
<Toaster />
|
||||
</div>
|
||||
<LoginUnlock />
|
||||
{stalker && (
|
||||
<div
|
||||
className="stalker"
|
||||
onClick={() => {
|
||||
LoginStore.removeSession(id);
|
||||
}}>
|
||||
<button type="button" className="btn btn-rnd">
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -240,7 +253,7 @@ function LogoHeader() {
|
||||
|
||||
return (
|
||||
<Link to="/" className="logo">
|
||||
<h1>{process.env.APP_NAME}</h1>
|
||||
<h1>{CONFIG.appName}</h1>
|
||||
{currentSubscription && (
|
||||
<small className="flex">
|
||||
<Icon name="diamond" size={10} className="mr5" />
|
||||
|
@ -143,7 +143,7 @@ export default function LoginPage() {
|
||||
|
||||
function generateNip46() {
|
||||
const meta = {
|
||||
name: process.env.APP_NAME_CAPITALIZED,
|
||||
name: CONFIG.appNameCapitalized,
|
||||
url: window.location.href,
|
||||
};
|
||||
|
||||
@ -287,7 +287,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<div className="login-container">
|
||||
<h1 className="logo" onClick={() => navigate("/")}>
|
||||
{process.env.APP_NAME}
|
||||
{CONFIG.appName}
|
||||
</h1>
|
||||
<h1 dir="auto">
|
||||
<FormattedMessage defaultMessage="Login" description="Login header" />
|
||||
@ -342,7 +342,7 @@ export default function LoginPage() {
|
||||
<FormattedMessage
|
||||
defaultMessage="Secure your private key with a PIN, ensuring enhanced protection on {site}. You'll be prompted to enter this PIN each time you access the site."
|
||||
values={{
|
||||
site: process.env.APP_NAME_CAPITALIZED,
|
||||
site: CONFIG.appNameCapitalized,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
@ -1,32 +1,33 @@
|
||||
import { NostrPrefix, tryParseNostrLink } from "@snort/system";
|
||||
import { useEffect, useState } from "react";
|
||||
import FormattedMessage from "Element/FormattedMessage";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { profileLink } from "SnortUtils";
|
||||
import { getNip05PubKey } from "Pages/LoginPage";
|
||||
import ProfilePage from "Pages/Profile/ProfilePage";
|
||||
import { ThreadRoute } from "Element/Event/Thread";
|
||||
|
||||
export default function NostrLinkHandler() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [renderComponent, setRenderComponent] = useState<React.ReactNode | null>(null);
|
||||
|
||||
const link = decodeURIComponent(params["*"] ?? "").toLowerCase();
|
||||
|
||||
async function handleLink(link: string) {
|
||||
const nav = tryParseNostrLink(link);
|
||||
if (nav) {
|
||||
if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
|
||||
navigate(`/e/${nav.encode()}`);
|
||||
setRenderComponent(<ThreadRoute id={nav.encode()} />); // Directly render ThreadRoute
|
||||
} else if (nav.type === NostrPrefix.PublicKey || nav.type === NostrPrefix.Profile) {
|
||||
navigate(`/p/${nav.encode()}`);
|
||||
setRenderComponent(<ProfilePage id={nav.encode()} />); // Directly render ProfilePage
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const pubkey = await getNip05PubKey(`${link}@${process.env.NIP05_DOMAIN}`);
|
||||
const pubkey = await getNip05PubKey(`${link}@${CONFIG.nip05Domain}`);
|
||||
if (pubkey) {
|
||||
navigate(profileLink(pubkey));
|
||||
setRenderComponent(<ProfilePage id={pubkey} />); // Directly render ProfilePage
|
||||
}
|
||||
} catch {
|
||||
//ignored
|
||||
@ -41,6 +42,10 @@ export default function NostrLinkHandler() {
|
||||
}
|
||||
}, [link]);
|
||||
|
||||
if (renderComponent) {
|
||||
return renderComponent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex f-center">
|
||||
{loading ? (
|
||||
|
@ -52,3 +52,29 @@
|
||||
max-width: 100%;
|
||||
max-height: 300px; /* Cap images in notifications to 300px height */
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--gray-light) !important;
|
||||
}
|
||||
|
||||
.summary-icon:not(.active):hover {
|
||||
background-color: var(--gray-dark);
|
||||
}
|
||||
|
||||
.summary-icon.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.summary-tooltip {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--gray-superdark);
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import "./Notifications.css";
|
||||
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
||||
import { EventExt, EventKind, NostrEvent, NostrLink, NostrPrefix, TaggedNostrEvent, parseZap } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { unixNow, unwrap } from "@snort/shared";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Bar, BarChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { markNotificationsRead } from "Login";
|
||||
import { Notifications, UserCache } from "Cache";
|
||||
import { dedupe, findTag, orderDescending } from "SnortUtils";
|
||||
import { dedupe, findTag, orderAscending, orderDescending } from "SnortUtils";
|
||||
import Icon from "Icons/Icon";
|
||||
import ProfileImage from "Element/User/ProfileImage";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
@ -20,6 +21,8 @@ import { formatShort } from "Number";
|
||||
import { LiveEvent } from "Element/LiveEvent";
|
||||
import ProfilePreview from "Element/User/ProfilePreview";
|
||||
import { getDisplayName } from "Element/User/DisplayName";
|
||||
import { Day } from "Const";
|
||||
import Tabs, { Tab } from "Element/Tabs";
|
||||
|
||||
function notificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
@ -77,10 +80,14 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
||||
return onHour.toString();
|
||||
};
|
||||
|
||||
const myNotifications = useMemo(() => {
|
||||
return orderDescending([...notifications]).filter(
|
||||
a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey),
|
||||
);
|
||||
}, [notifications, login.publicKey]);
|
||||
|
||||
const timeGrouped = useMemo(() => {
|
||||
return orderDescending([...notifications])
|
||||
.filter(a => !isMuted(a.pubkey) && a.tags.some(b => b[0] === "p" && b[1] === login.publicKey))
|
||||
.reduce((acc, v) => {
|
||||
return myNotifications.reduce((acc, v) => {
|
||||
const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`;
|
||||
if (acc.has(key)) {
|
||||
unwrap(acc.get(key)).push(v as TaggedNostrEvent);
|
||||
@ -89,16 +96,179 @@ export default function NotificationsPage({ onClick }: { onClick?: (link: NostrL
|
||||
}
|
||||
return acc;
|
||||
}, new Map<string, Array<TaggedNostrEvent>>());
|
||||
}, [notifications]);
|
||||
}, [myNotifications]);
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<NotificationSummary evs={myNotifications as TaggedNostrEvent[]} />
|
||||
|
||||
{login.publicKey &&
|
||||
[...timeGrouped.entries()].map(([k, g]) => <NotificationGroup key={k} evs={g} onClick={onClick} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatSlot {
|
||||
time: string;
|
||||
reactions: number;
|
||||
reposts: number;
|
||||
quotes: number;
|
||||
mentions: number;
|
||||
zaps: number;
|
||||
}
|
||||
|
||||
const enum NotificationSummaryPeriod {
|
||||
Daily,
|
||||
Weekly,
|
||||
}
|
||||
|
||||
const enum NotificationSummaryFilter {
|
||||
Reactions = 1,
|
||||
Reposts = 2,
|
||||
Mentions = 4,
|
||||
Zaps = 8,
|
||||
All = 255,
|
||||
}
|
||||
|
||||
function NotificationSummary({ evs }: { evs: Array<TaggedNostrEvent> }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [period, setPeriod] = useState(NotificationSummaryPeriod.Daily);
|
||||
const [filter, setFilter] = useState(NotificationSummaryFilter.All);
|
||||
|
||||
const periodTabs = [
|
||||
{
|
||||
value: NotificationSummaryPeriod.Daily,
|
||||
text: <FormattedMessage defaultMessage="Daily" />,
|
||||
},
|
||||
{
|
||||
value: NotificationSummaryPeriod.Weekly,
|
||||
text: <FormattedMessage defaultMessage="Weekly" />,
|
||||
},
|
||||
] as Array<Tab>;
|
||||
|
||||
const hasFlag = (v: number, f: NotificationSummaryFilter) => {
|
||||
return (v & f) > 0;
|
||||
};
|
||||
|
||||
const getWeek = (d: Date) => {
|
||||
const onejan = new Date(d.getFullYear(), 0, 1);
|
||||
const today = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
const dayOfYear = (today.getTime() - onejan.getTime() + 86400000) / 86400000;
|
||||
return Math.ceil(dayOfYear / 7);
|
||||
};
|
||||
|
||||
const stats = useMemo(() => {
|
||||
return orderAscending(evs)
|
||||
.filter(a => (period === NotificationSummaryPeriod.Daily ? a.created_at > unixNow() - 14 * Day : true))
|
||||
.reduce(
|
||||
(acc, v) => {
|
||||
const date = new Date(v.created_at * 1000);
|
||||
const key =
|
||||
period === NotificationSummaryPeriod.Daily
|
||||
? `${date.getMonth() + 1}/${date.getDate()}`
|
||||
: getWeek(date).toString();
|
||||
acc[key] ??= {
|
||||
time: key,
|
||||
reactions: 0,
|
||||
reposts: 0,
|
||||
quotes: 0,
|
||||
mentions: 0,
|
||||
zaps: 0,
|
||||
};
|
||||
|
||||
if (v.kind === EventKind.Reaction) {
|
||||
acc[key].reactions++;
|
||||
} else if (v.kind === EventKind.Repost) {
|
||||
acc[key].reposts++;
|
||||
} else if (v.kind === EventKind.ZapReceipt) {
|
||||
acc[key].zaps++;
|
||||
}
|
||||
if (v.kind === EventKind.TextNote) {
|
||||
acc[key].mentions++;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, StatSlot>,
|
||||
);
|
||||
}, [evs, period]);
|
||||
|
||||
const filterIcon = (f: NotificationSummaryFilter, icon: string, iconActiveClass?: string) => {
|
||||
const active = hasFlag(filter, f);
|
||||
return (
|
||||
<div className={`summary-icon${active ? " active" : ""}`} onClick={() => setFilter(v => v ^ f)}>
|
||||
<Icon name={icon} className={active ? iconActiveClass : undefined} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-column g12 p bb">
|
||||
<div className="flex f-space">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Summary" description="Notifications summary" />
|
||||
</h2>
|
||||
<div className="flex g8">
|
||||
{filterIcon(NotificationSummaryFilter.Reactions, "heart-solid", "text-heart")}
|
||||
{filterIcon(NotificationSummaryFilter.Zaps, "zap-solid", "text-zap")}
|
||||
{filterIcon(NotificationSummaryFilter.Reposts, "reverse-left", "text-repost")}
|
||||
{filterIcon(NotificationSummaryFilter.Mentions, "at-sign", "text-mention")}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs tabs={periodTabs} tab={unwrap(periodTabs.find(a => a.value === period))} setTab={t => setPeriod(t.value)} />
|
||||
<div ref={ref}>
|
||||
<BarChart
|
||||
width={ref.current?.clientWidth}
|
||||
height={200}
|
||||
data={Object.values(stats)}
|
||||
margin={{ left: 0, right: 0 }}
|
||||
style={{ userSelect: "none" }}>
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
{hasFlag(filter, NotificationSummaryFilter.Reactions) && (
|
||||
<Bar dataKey="reactions" fill="var(--heart)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Reposts) && (
|
||||
<Bar dataKey="reposts" fill="var(--repost)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Mentions) && (
|
||||
<Bar dataKey="mentions" fill="var(--mention)" stackId="" />
|
||||
)}
|
||||
{hasFlag(filter, NotificationSummaryFilter.Zaps) && <Bar dataKey="zaps" fill="var(--zap)" stackId="" />}
|
||||
<Tooltip
|
||||
cursor={{ fill: "rgba(255,255,255,0.2)" }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="summary-tooltip">
|
||||
<div className="flex-column g12">
|
||||
<Icon name="heart-solid" className="text-heart" />
|
||||
{formatShort(payload.find(a => a.name === "reactions")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="zap-solid" className="text-zap" />
|
||||
{formatShort(payload.find(a => a.name === "zaps")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="reverse-left" className="text-repost" />
|
||||
{formatShort(payload.find(a => a.name === "reposts")?.value as number)}
|
||||
</div>
|
||||
<div className="flex-column g12">
|
||||
<Icon name="at-sign" className="text-mention" />
|
||||
{formatShort(payload.find(a => a.name === "mentions")?.value as number)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationGroup({ evs, onClick }: { evs: Array<TaggedNostrEvent>; onClick?: (link: NostrLink) => void }) {
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
|
@ -138,14 +138,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profile .website a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.profile .website a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile .link svg {
|
||||
color: var(--highlight);
|
||||
}
|
@ -2,36 +2,19 @@ import "./ProfilePage.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import FormattedMessage from "Element/FormattedMessage";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
encodeTLV,
|
||||
encodeTLVEntries,
|
||||
EventKind,
|
||||
HexKey,
|
||||
NostrLink,
|
||||
NostrPrefix,
|
||||
TLVEntryType,
|
||||
tryParseNostrLink,
|
||||
} from "@snort/system";
|
||||
import { encodeTLV, encodeTLVEntries, EventKind, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
|
||||
import { LNURL } from "@snort/shared";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import { findTag, getReactions, unwrap } from "SnortUtils";
|
||||
import { formatShort } from "Number";
|
||||
import Note from "Element/Event/Note";
|
||||
import Bookmarks from "Element/Bookmarks";
|
||||
import RelaysMetadata from "Element/Relay/RelaysMetadata";
|
||||
import { Tab, TabElement } from "Element/Tabs";
|
||||
import Icon from "Icons/Icon";
|
||||
import useMutedFeed from "Feed/MuteList";
|
||||
import useRelaysFeed from "Feed/RelaysFeed";
|
||||
import usePinnedFeed from "Feed/PinnedFeed";
|
||||
import useBookmarkFeed from "Feed/BookmarkFeed";
|
||||
import useFollowersFeed from "Feed/FollowersFeed";
|
||||
import useFollowsFeed from "Feed/FollowsFeed";
|
||||
import useProfileBadges from "Feed/BadgesFeed";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useZapsFeed from "Feed/ZapsFeed";
|
||||
import { default as ZapElement } from "Element/Event/Zap";
|
||||
import FollowButton from "Element/User/FollowButton";
|
||||
import { parseId, hexToBech32 } from "SnortUtils";
|
||||
import Avatar from "Element/User/Avatar";
|
||||
@ -57,61 +40,24 @@ import useLogin from "Hooks/useLogin";
|
||||
import { ZapTarget } from "Zapper";
|
||||
import { useStatusFeed } from "Feed/StatusFeed";
|
||||
|
||||
import messages from "./messages";
|
||||
import messages from "../messages";
|
||||
import { SpotlightMediaModal } from "Element/Deck/SpotlightMedia";
|
||||
import ProfileTab, {
|
||||
BookMarksTab,
|
||||
FollowersTab,
|
||||
FollowsTab,
|
||||
ProfileTabType,
|
||||
RelaysTab,
|
||||
ZapsProfileTab,
|
||||
} from "Pages/Profile/ProfileTab";
|
||||
import DisplayName from "../../Element/User/DisplayName";
|
||||
import { UserWebsiteLink } from "Element/User/UserWebsiteLink";
|
||||
|
||||
const NOTES = 0;
|
||||
const REACTIONS = 1;
|
||||
const FOLLOWERS = 2;
|
||||
const FOLLOWS = 3;
|
||||
const ZAPS = 4;
|
||||
const MUTED = 5;
|
||||
const BLOCKED = 6;
|
||||
const RELAYS = 7;
|
||||
const BOOKMARKS = 8;
|
||||
|
||||
function ZapsProfileTab({ id }: { id: HexKey }) {
|
||||
const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
|
||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2 className="p">
|
||||
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
|
||||
</h2>
|
||||
{zaps.map(z => (
|
||||
<ZapElement showZapped={false} zap={z} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
interface ProfilePageProps {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function FollowersTab({ id }: { id: HexKey }) {
|
||||
const followers = useFollowersFeed(id);
|
||||
return <FollowsList pubkeys={followers} showAbout={true} className="p" />;
|
||||
}
|
||||
|
||||
function FollowsTab({ id }: { id: HexKey }) {
|
||||
const follows = useFollowsFeed(id);
|
||||
return <FollowsList pubkeys={follows} showAbout={true} className="p" />;
|
||||
}
|
||||
|
||||
function RelaysTab({ id }: { id: HexKey }) {
|
||||
const relays = useRelaysFeed(id);
|
||||
return <RelaysMetadata relays={relays} />;
|
||||
}
|
||||
|
||||
function BookMarksTab({ id }: { id: HexKey }) {
|
||||
const bookmarks = useBookmarkFeed(id);
|
||||
return (
|
||||
<Bookmarks
|
||||
pubkey={id}
|
||||
bookmarks={bookmarks.filter(e => e.kind === EventKind.TextNote)}
|
||||
related={bookmarks.filter(e => e.kind !== EventKind.TextNote)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
export default function ProfilePage({ id: propId }: ProfilePageProps) {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState<string>();
|
||||
@ -135,8 +81,6 @@ export default function ProfilePage() {
|
||||
const showBadges = login.preferences.showBadges ?? false;
|
||||
const showStatus = login.preferences.showStatus ?? true;
|
||||
|
||||
const website_url =
|
||||
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
|
||||
// feeds
|
||||
const { blocked } = useModeration();
|
||||
const pinned = usePinnedFeed(id);
|
||||
@ -146,89 +90,6 @@ export default function ProfilePage() {
|
||||
const status = useStatusFeed(showStatus ? id : undefined, true);
|
||||
|
||||
// tabs
|
||||
const ProfileTab = {
|
||||
Notes: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="pencil" size={16} />
|
||||
<FormattedMessage defaultMessage="Notes" />
|
||||
</>
|
||||
),
|
||||
value: NOTES,
|
||||
},
|
||||
Reactions: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="reaction" size={16} />
|
||||
<FormattedMessage defaultMessage="Reactions" />
|
||||
</>
|
||||
),
|
||||
value: REACTIONS,
|
||||
},
|
||||
Followers: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="user-v2" size={16} />
|
||||
<FormattedMessage defaultMessage="Followers" />
|
||||
</>
|
||||
),
|
||||
value: FOLLOWERS,
|
||||
},
|
||||
Follows: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="stars" size={16} />
|
||||
<FormattedMessage defaultMessage="Follows" />
|
||||
</>
|
||||
),
|
||||
value: FOLLOWS,
|
||||
},
|
||||
Zaps: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="zap-solid" size={16} />
|
||||
<FormattedMessage defaultMessage="Zaps" />
|
||||
</>
|
||||
),
|
||||
value: ZAPS,
|
||||
},
|
||||
Muted: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="mute" size={16} />
|
||||
<FormattedMessage defaultMessage="Muted" />
|
||||
</>
|
||||
),
|
||||
value: MUTED,
|
||||
},
|
||||
Blocked: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="block" size={16} />
|
||||
<FormattedMessage defaultMessage="Blocked" />
|
||||
</>
|
||||
),
|
||||
value: BLOCKED,
|
||||
},
|
||||
Relays: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="wifi" size={16} />
|
||||
<FormattedMessage defaultMessage="Relays" />
|
||||
</>
|
||||
),
|
||||
value: RELAYS,
|
||||
},
|
||||
Bookmarks: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="bookmark-solid" size={16} />
|
||||
<FormattedMessage defaultMessage="Bookmarks" />
|
||||
</>
|
||||
),
|
||||
value: BOOKMARKS,
|
||||
},
|
||||
} as { [key: string]: Tab };
|
||||
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
||||
const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
|
||||
unwrap(a),
|
||||
@ -236,21 +97,22 @@ export default function ProfilePage() {
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id?.match(EmailRegex)) {
|
||||
getNip05PubKey(params.id).then(a => {
|
||||
const resolvedId = propId || params.id;
|
||||
if (resolvedId?.match(EmailRegex)) {
|
||||
getNip05PubKey(resolvedId).then(a => {
|
||||
setId(a);
|
||||
});
|
||||
} else {
|
||||
const nav = tryParseNostrLink(params.id ?? "");
|
||||
const nav = tryParseNostrLink(resolvedId ?? "");
|
||||
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
|
||||
// todo: use relays if any for nprofile
|
||||
setId(nav.id);
|
||||
} else {
|
||||
setId(parseId(params.id ?? ""));
|
||||
setId(parseId(resolvedId ?? ""));
|
||||
}
|
||||
}
|
||||
setTab(ProfileTab.Notes);
|
||||
}, [params]);
|
||||
}, [propId, params]);
|
||||
|
||||
function musicStatus() {
|
||||
if (!status.music) return;
|
||||
@ -275,12 +137,21 @@ export default function ProfilePage() {
|
||||
return inner();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.nip05 && user?.isNostrAddressValid) {
|
||||
if (user.nip05.endsWith(`@${CONFIG.nip05Domain}`)) {
|
||||
const username = user.nip05?.replace(`@${CONFIG.nip05Domain}`, "");
|
||||
navigate(`/${username}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}, [user?.isNostrAddressValid, user?.nip05]);
|
||||
|
||||
function username() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-column g4">
|
||||
<h2 className="flex g4">
|
||||
{user?.display_name || user?.name || "Nostrich"}
|
||||
<DisplayName user={user} pubkey={user?.pubkey ?? ""} />
|
||||
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
|
||||
</h2>
|
||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||
@ -295,28 +166,10 @@ export default function ProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
function tryFormatWebsite(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.hostname}${u.pathname !== "/" ? u.pathname : ""}`;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function links() {
|
||||
return (
|
||||
<>
|
||||
{user?.website && (
|
||||
<div className="link website f-ellipsis">
|
||||
<Icon name="link-02" size={16} />
|
||||
<a href={website_url} target="_blank" rel="noreferrer">
|
||||
{tryFormatWebsite(user.website)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UserWebsiteLink user={user} />
|
||||
{lnurl && (
|
||||
<div className="link lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
|
||||
<Icon name="zapCircle" size={16} />
|
||||
@ -369,7 +222,7 @@ export default function ProfilePage() {
|
||||
if (!id) return null;
|
||||
|
||||
switch (tab.value) {
|
||||
case NOTES:
|
||||
case ProfileTabType.NOTES:
|
||||
return (
|
||||
<>
|
||||
{pinned
|
||||
@ -399,29 +252,29 @@ export default function ProfilePage() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case ZAPS: {
|
||||
case ProfileTabType.ZAPS: {
|
||||
return <ZapsProfileTab id={id} />;
|
||||
}
|
||||
case FOLLOWS: {
|
||||
case ProfileTabType.FOLLOWS: {
|
||||
if (isMe) {
|
||||
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} className="p" />;
|
||||
} else {
|
||||
return <FollowsTab id={id} />;
|
||||
}
|
||||
}
|
||||
case FOLLOWERS: {
|
||||
case ProfileTabType.FOLLOWERS: {
|
||||
return <FollowersTab id={id} />;
|
||||
}
|
||||
case MUTED: {
|
||||
case ProfileTabType.MUTED: {
|
||||
return <MutedList pubkeys={muted} />;
|
||||
}
|
||||
case BLOCKED: {
|
||||
case ProfileTabType.BLOCKED: {
|
||||
return <BlockList />;
|
||||
}
|
||||
case RELAYS: {
|
||||
case ProfileTabType.RELAYS: {
|
||||
return <RelaysTab id={id} />;
|
||||
}
|
||||
case BOOKMARKS: {
|
||||
case ProfileTabType.BOOKMARKS: {
|
||||
return <BookMarksTab id={id} />;
|
||||
}
|
||||
}
|
||||
@ -433,7 +286,7 @@ export default function ProfilePage() {
|
||||
<Avatar pubkey={id ?? ""} user={user} onClick={() => setModalImage(user?.picture || "")} className="pointer" />
|
||||
<div className="profile-actions">
|
||||
{renderIcons()}
|
||||
{!isMe && id && <FollowButton className="primary" pubkey={id} />}
|
||||
{!isMe && id && <FollowButton pubkey={id} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
154
packages/app/src/Pages/Profile/ProfileTab.tsx
Normal file
154
packages/app/src/Pages/Profile/ProfileTab.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import useZapsFeed from "../../Feed/ZapsFeed";
|
||||
import FormattedMessage from "../../Element/FormattedMessage";
|
||||
import messages from "../messages";
|
||||
import { formatShort } from "../../Number";
|
||||
import useFollowersFeed from "../../Feed/FollowersFeed";
|
||||
import FollowsList from "../../Element/User/FollowListBase";
|
||||
import useFollowsFeed from "../../Feed/FollowsFeed";
|
||||
import useRelaysFeed from "../../Feed/RelaysFeed";
|
||||
import RelaysMetadata from "../../Element/Relay/RelaysMetadata";
|
||||
import useBookmarkFeed from "../../Feed/BookmarkFeed";
|
||||
import Bookmarks from "../../Element/Bookmarks";
|
||||
import Icon from "../../Icons/Icon";
|
||||
import { Tab } from "../../Element/Tabs";
|
||||
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system";
|
||||
import { default as ZapElement } from "Element/Event/Zap";
|
||||
|
||||
export enum ProfileTabType {
|
||||
NOTES = 0,
|
||||
REACTIONS = 1,
|
||||
FOLLOWERS = 2,
|
||||
FOLLOWS = 3,
|
||||
ZAPS = 4,
|
||||
MUTED = 5,
|
||||
BLOCKED = 6,
|
||||
RELAYS = 7,
|
||||
BOOKMARKS = 8,
|
||||
}
|
||||
|
||||
export function ZapsProfileTab({ id }: { id: HexKey }) {
|
||||
const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
|
||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2 className="p">
|
||||
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
|
||||
</h2>
|
||||
{zaps.map(z => (
|
||||
<ZapElement showZapped={false} zap={z} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FollowersTab({ id }: { id: HexKey }) {
|
||||
const followers = useFollowersFeed(id);
|
||||
return <FollowsList pubkeys={followers} showAbout={true} className="p" />;
|
||||
}
|
||||
|
||||
export function FollowsTab({ id }: { id: HexKey }) {
|
||||
const follows = useFollowsFeed(id);
|
||||
return <FollowsList pubkeys={follows} showAbout={true} className="p" />;
|
||||
}
|
||||
|
||||
export function RelaysTab({ id }: { id: HexKey }) {
|
||||
const relays = useRelaysFeed(id);
|
||||
return <RelaysMetadata relays={relays} />;
|
||||
}
|
||||
|
||||
export function BookMarksTab({ id }: { id: HexKey }) {
|
||||
const bookmarks = useBookmarkFeed(id);
|
||||
return (
|
||||
<Bookmarks
|
||||
pubkey={id}
|
||||
bookmarks={bookmarks.filter(e => e.kind === EventKind.TextNote)}
|
||||
related={bookmarks.filter(e => e.kind !== EventKind.TextNote)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ProfileTab = {
|
||||
Notes: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="pencil" size={16} />
|
||||
<FormattedMessage defaultMessage="Notes" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.NOTES,
|
||||
},
|
||||
Reactions: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="reaction" size={16} />
|
||||
<FormattedMessage defaultMessage="Reactions" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.REACTIONS,
|
||||
},
|
||||
Followers: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="user-v2" size={16} />
|
||||
<FormattedMessage defaultMessage="Followers" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.FOLLOWERS,
|
||||
},
|
||||
Follows: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="stars" size={16} />
|
||||
<FormattedMessage defaultMessage="Follows" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.FOLLOWS,
|
||||
},
|
||||
Zaps: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="zap-solid" size={16} />
|
||||
<FormattedMessage defaultMessage="Zaps" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.ZAPS,
|
||||
},
|
||||
Muted: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="mute" size={16} />
|
||||
<FormattedMessage defaultMessage="Muted" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.MUTED,
|
||||
},
|
||||
Blocked: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="block" size={16} />
|
||||
<FormattedMessage defaultMessage="Blocked" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.BLOCKED,
|
||||
},
|
||||
Relays: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="wifi" size={16} />
|
||||
<FormattedMessage defaultMessage="Relays" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.RELAYS,
|
||||
},
|
||||
Bookmarks: {
|
||||
text: (
|
||||
<>
|
||||
<Icon name="bookmark-solid" size={16} />
|
||||
<FormattedMessage defaultMessage="Bookmarks" />
|
||||
</>
|
||||
),
|
||||
value: ProfileTabType.BOOKMARKS,
|
||||
},
|
||||
} as { [key: string]: Tab };
|
||||
|
||||
export default ProfileTab;
|
@ -14,11 +14,11 @@ export default defineMessages({
|
||||
KeysSaved: { defaultMessage: "I have saved my keys, continue" },
|
||||
WhatIsSnort: {
|
||||
defaultMessage: "What is {site} and how does it work?",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
WhatIsSnortIntro: {
|
||||
defaultMessage: `{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`,
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
WhatIsSnortNotes: {
|
||||
defaultMessage: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`,
|
||||
@ -26,7 +26,7 @@ export default defineMessages({
|
||||
|
||||
WhatIsSnortExperience: {
|
||||
defaultMessage: "{site} is designed to have a similar experience to Twitter.",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
HowKeysWork: { defaultMessage: "How do keys work?" },
|
||||
DigitalSignatures: {
|
||||
@ -70,9 +70,9 @@ export default defineMessages({
|
||||
NameSquatting: {
|
||||
defaultMessage:
|
||||
"Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.",
|
||||
values: { site: process.env.APP_NAME_CAPITALIZED },
|
||||
values: { site: CONFIG.appNameCapitalized },
|
||||
},
|
||||
PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: process.env.APP_NAME_CAPITALIZED } },
|
||||
PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: CONFIG.appNameCapitalized } },
|
||||
GetSnortId: { defaultMessage: "Get a Snort identifier" },
|
||||
GetSnortIdHelp: {
|
||||
defaultMessage:
|
||||
|
@ -6,9 +6,9 @@ import Icon from "Icons/Icon";
|
||||
import { LoginStore, logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
|
||||
import messages from "./messages";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const login = useLogin();
|
||||
@ -61,11 +61,13 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Nostr Adddress" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
{CONFIG.features.subscriptions && (
|
||||
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
|
||||
<Icon name="diamond" size={24} />
|
||||
<FormattedMessage defaultMessage="Subscription" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
)}
|
||||
{sub && (
|
||||
<div className="settings-row" onClick={() => navigate("accounts")}>
|
||||
<Icon name="code-circle" size={24} />
|
||||
|
@ -328,6 +328,10 @@ export function orderDescending<T>(arr: Array<T & { created_at: number }>) {
|
||||
return arr.sort((a, b) => (b.created_at > a.created_at ? 1 : -1));
|
||||
}
|
||||
|
||||
export function orderAscending<T>(arr: Array<T & { created_at: number }>) {
|
||||
return arr.sort((a, b) => (b.created_at > a.created_at ? -1 : 1));
|
||||
}
|
||||
|
||||
export interface Magnet {
|
||||
dn?: string | string[];
|
||||
tr?: string | string[];
|
||||
|
@ -15,7 +15,7 @@ export class DonateTask extends BaseUITask {
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Thanks for using {site}, please consider donating if you can."
|
||||
values={{ site: process.env.APP_NAME_CAPITALIZED }}
|
||||
values={{ site: CONFIG.appNameCapitalized }}
|
||||
/>
|
||||
</p>
|
||||
<Link to="/donate">
|
||||
|
@ -15,6 +15,8 @@
|
||||
--live: #f83838;
|
||||
--heart: #ef4444;
|
||||
--zap: #ff710a;
|
||||
--mention: #961ee1;
|
||||
--repost: #1ecbe1;
|
||||
--gray-superlight: #eee;
|
||||
--bg-secondary: #2a2a2a;
|
||||
--gray-light: #999;
|
||||
@ -105,12 +107,28 @@ body {
|
||||
color: var(--font-color);
|
||||
font-size: var(--font-size);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
line-height: 1.3em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.ext {
|
||||
word-break: break-all;
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
#root {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -137,6 +155,10 @@ code {
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.bb {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
@ -149,6 +171,10 @@ code {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.p4 {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.p24 {
|
||||
padding: 24px;
|
||||
}
|
||||
@ -488,21 +514,6 @@ input:disabled {
|
||||
max-width: -moz-available;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
line-height: 1.3em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.ext {
|
||||
word-break: break-all;
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
div.form {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
@ -619,6 +630,38 @@ div.form-col {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.bg-zap {
|
||||
background-color: var(--zap);
|
||||
}
|
||||
|
||||
.bg-heart {
|
||||
background-color: var(--heart);
|
||||
}
|
||||
|
||||
.bg-mention {
|
||||
background-color: var(--mention);
|
||||
}
|
||||
|
||||
.bg-repost {
|
||||
background-color: var(--repost);
|
||||
}
|
||||
|
||||
.text-zap {
|
||||
color: var(--zap);
|
||||
}
|
||||
|
||||
.text-heart {
|
||||
color: var(--heart);
|
||||
}
|
||||
|
||||
.text-mention {
|
||||
color: var(--mention);
|
||||
}
|
||||
|
||||
.text-repost {
|
||||
color: var(--repost);
|
||||
}
|
||||
|
||||
.tweet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -664,11 +707,6 @@ div.form-col {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
line-height: 36px;
|
||||
margin: 12px 0 0 0;
|
||||
}
|
||||
|
||||
.main-content .h4 {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.main-content .profile-preview {
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
ReqFilter,
|
||||
PowMiner,
|
||||
NostrEvent,
|
||||
mapEventToProfile,
|
||||
} from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
@ -24,7 +25,7 @@ import { IntlProvider } from "IntlProvider";
|
||||
import { unwrap } from "SnortUtils";
|
||||
import Layout from "Pages/Layout";
|
||||
import LoginPage from "Pages/LoginPage";
|
||||
import ProfilePage from "Pages/ProfilePage";
|
||||
import ProfilePage from "Pages/Profile/ProfilePage";
|
||||
import { RootRoutes, RootTabRoutes } from "Pages/Root";
|
||||
import NotificationsPage from "Pages/Notifications";
|
||||
import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage";
|
||||
@ -42,7 +43,7 @@ import { SubscribeRoutes } from "Pages/subscribe";
|
||||
import ZapPoolPage from "Pages/ZapPool";
|
||||
import DebugPage from "Pages/Debug";
|
||||
import { db } from "Db";
|
||||
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
||||
import { preload, RelayMetrics, SystemDb, UserCache, UserRelays } from "Cache";
|
||||
import { LoginStore } from "Login";
|
||||
import { SnortDeckLayout } from "Pages/DeckLayout";
|
||||
import FreeNostrAddressPage from "./Pages/FreeNostrAddressPage";
|
||||
@ -77,6 +78,7 @@ export const System = new NostrSystem({
|
||||
profileCache: UserCache,
|
||||
relayMetrics: RelayMetrics,
|
||||
queryOptimizer: WasmQueryOptimizer,
|
||||
db: SystemDb,
|
||||
authHandler: async (c, r) => {
|
||||
const { id } = LoginStore.snapshot();
|
||||
const pub = LoginStore.getPublisher(id);
|
||||
@ -86,6 +88,29 @@ export const System = new NostrSystem({
|
||||
},
|
||||
});
|
||||
|
||||
async function fetchProfile(key: string) {
|
||||
const rsp = await fetch(`${CONFIG.httpCache}/profile/${key}`);
|
||||
if (rsp.ok) {
|
||||
try {
|
||||
const data = (await rsp.json()) as NostrEvent;
|
||||
if (data) {
|
||||
return mapEventToProfile(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add profile loader fn
|
||||
*/
|
||||
if (CONFIG.httpCache) {
|
||||
System.ProfileLoader.loaderFn = async (keys: Array<string>) => {
|
||||
return (await Promise.all(keys.map(a => fetchProfile(a)))).filter(a => a !== undefined).map(a => unwrap(a));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton user profile loader
|
||||
*/
|
||||
@ -190,7 +215,7 @@ export const router = createBrowserRouter([
|
||||
},
|
||||
...NewUserRoutes,
|
||||
...WalletRoutes,
|
||||
...SubscribeRoutes,
|
||||
...(CONFIG.features.subscriptions ? SubscribeRoutes : []),
|
||||
{
|
||||
path: "/debug",
|
||||
element: <DebugPage />,
|
||||
|
@ -42,6 +42,9 @@
|
||||
"/Xf4UW": {
|
||||
"defaultMessage": "Send anonymous usage metrics"
|
||||
},
|
||||
"/clOBU": {
|
||||
"defaultMessage": "Weekly"
|
||||
},
|
||||
"/d6vEc": {
|
||||
"defaultMessage": "Make your profile easier to find and share"
|
||||
},
|
||||
@ -704,6 +707,10 @@
|
||||
"PCSt5T": {
|
||||
"defaultMessage": "Preferences"
|
||||
},
|
||||
"PJeJFc": {
|
||||
"defaultMessage": "Summary",
|
||||
"description": "Notifications summary"
|
||||
},
|
||||
"PLSbmL": {
|
||||
"defaultMessage": "Your mnemonic phrase"
|
||||
},
|
||||
@ -1508,5 +1515,8 @@
|
||||
},
|
||||
"zwb6LR": {
|
||||
"defaultMessage": "<b>Mint:</b> {url}"
|
||||
},
|
||||
"zxvhnE": {
|
||||
"defaultMessage": "Daily"
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "علنية",
|
||||
"/RD0e2": "يستخدم نوستر تقنية التوقيع الرقمي لنشر المنشورات دون أي تلاعب ويمكن إرسالها بأمان إلى العديد من الموصلات لتكرار تخزينها وضمان استمرارية المحتوى الخاص بك.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "اجعل ملفك الشخصي أسهل في العثور عليه ومشاركته",
|
||||
"/n5KSF": "{n} مللي ثانية",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "مطابق لاعدادات جهازك",
|
||||
"P7nJT9": "الإجمالي اليوم (بالتوقيت العالمي ): {amount} ساتوشي",
|
||||
"PCSt5T": "التفضيلات",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "عبارة الاسترداد الخاصة بك",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "عنوان الملف غير معروف: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "انت جاهز!",
|
||||
"zonsdq": "فشل تحميل خدمة LNURL",
|
||||
"zvCDao": "تظهر تلقائيا أحدث الملاحظات",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "İctimai",
|
||||
"/RD0e2": "Nostr, məzmununuzun lazımsız saxlanmasını təmin etmək üçün bir çox relelərə təhlükəsiz şəkildə təkrarlana bilən saxta qeydləri təmin etmək üçün rəqəmsal imza texnologiyasından istifadə edir.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Profilinizi tapmağı və paylaşmağı asanlaşdırın",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Daha çox",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "System (Default)",
|
||||
"P7nJT9": "Total today (UTC): {amount} sats",
|
||||
"PCSt5T": "Preferences",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Your mnemonic phrase",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Unknown file header: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Hazırsan!",
|
||||
"zonsdq": "LNURL xidmətini yükləmək alınmadı",
|
||||
"zvCDao": "Ən son qeydləri avtomatik göstərin",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Öffentlich",
|
||||
"/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": "Anonyme Nutzungsmetriken senden",
|
||||
"/clOBU": "Wöchentlich",
|
||||
"/d6vEc": "Mach dein Profil leichter zu finden und zu teilen",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Mehr laden",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "System (Standard)",
|
||||
"P7nJT9": "Gesamt heute (UTC): {amount} sats",
|
||||
"PCSt5T": "Einstellungen",
|
||||
"PJeJFc": "Übersicht",
|
||||
"PLSbmL": "Ihre mnemonische Passphrase",
|
||||
"PaN7t3": "Vorschau auf {site}",
|
||||
"PamNxw": "Unbekannter Datei-Header: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Du bist bereit!",
|
||||
"zonsdq": "Fehler beim Laden des LNURL-Dienstes",
|
||||
"zvCDao": "Neueste Notes automatisch anzeigen",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Täglich"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Public",
|
||||
"/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Make your profile easier to find and share",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "System (Default)",
|
||||
"P7nJT9": "Total today (UTC): {amount} sats",
|
||||
"PCSt5T": "Preferences",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Your mnemonic phrase",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Unknown file header: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "You're ready!",
|
||||
"zonsdq": "Failed to load LNURL service",
|
||||
"zvCDao": "Automatically show latest notes",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Público",
|
||||
"/RD0e2": "Nostr utiliza firmas digitales para proveer de notas inmutables que pueden ser replicadas a muchos relays para que tu contenido esté distribuido redundantemente.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Haz tu perfil más fácil de encontrar y compartir",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Sistema (por defecto)",
|
||||
"P7nJT9": "Total hoy (UTC): {amount} sáb",
|
||||
"PCSt5T": "Preferencias",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Frase mnemotécnica",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Encabezado de archivo desconocido: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Estás listo!",
|
||||
"zonsdq": "Error al contactar con el servicio LNURL",
|
||||
"zvCDao": "Mostrar notas nuevas automáticamente",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "عمومی",
|
||||
"/RD0e2": "ناستر از تکنولوژی امضای دیجیتال برای فراهم نمودن یادداشت های غیرقابل دستکاری استفاده می کند که می تواند به طور امن در رله های زیادی تکرار شود تا انبار اضافی برای محتوایتان تامین کند.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "یافتن و اشتراک گذاری نمایه خود را آسان تر کنید",
|
||||
"/n5KSF": "{n} میلی ثانیه",
|
||||
"00LcfG": "بارگیری بیشتر",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "سیستم (پیش فرض)",
|
||||
"P7nJT9": "جمع امروز (UTC): {amount} ساتوشی",
|
||||
"PCSt5T": "ترجیحات",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "عبارت بازیابی یادسپاری شما",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "سرفایل ناشناس: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "شما آماده هستید!",
|
||||
"zonsdq": "خدمات LNURL بارگیری نشد",
|
||||
"zvCDao": "به طور خودکار یادداشت های اخیر را نشان بده",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Julkinen",
|
||||
"/RD0e2": "Nostr käyttää digitaalista allekirjoitusteknologiaa tarjotakseen väärentämättömiä viestejä, joita voidaan turvallisesti replikoida monille välittäjille tarjoten näin varastointia sisällöllesi.",
|
||||
"/Xf4UW": "Lähetä nimettömiä käyttötietoja",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Tee profiilistasi helpompi löytää ja jakaa",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Lataa lisää",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Järjestelmä (oletus)",
|
||||
"P7nJT9": "Tänään yhteensä (UTC): {amount} satsia",
|
||||
"PCSt5T": "Asetukset",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Sinun mnemonic-lauseesi",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Tuntematon tiedostotunniste: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Olet valmis!",
|
||||
"zonsdq": "Epäonnistunut LNURL-palvelun lataus",
|
||||
"zvCDao": "Näytä uusimmat viestit automaattisesti",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Publique",
|
||||
"/RD0e2": "Nostr utilise la technologie de signature numérique pour fournir des notes inviolables qui peuvent être répliquées en toute sécurité sur de nombreux relais pour fournir un stockage redondant de votre contenu.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Rendez votre profil plus facile à trouver et à partager",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Charger plus",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Système (Défaut)",
|
||||
"P7nJT9": "Total aujourd'hui (UTC) : {amount} sats",
|
||||
"PCSt5T": "Préférences",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Votre phrase mnémonique",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "En-tête du fichier inconnu : {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Tu es prêt!",
|
||||
"zonsdq": "Échec du chargement du service LNURL",
|
||||
"zvCDao": "Afficher automatiquement les dernières notes",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Javno",
|
||||
"/RD0e2": "Nostr koristi tehnologiju digitalnog potpisa kako bi pružio bilješke o zaštiti od neovlaštenog mijenjanja koje se mogu sigurno replicirati na mnoge releje kako bi se osigurala nepotrebna pohrana vašeg sadržaja.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Olakšajte pronalazak i dijeljenje svog profila",
|
||||
"/n5KSF": "{n} mikrosekundi",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Sustav (zadano)",
|
||||
"P7nJT9": "Ukupno danas (UTC): {amount} sati",
|
||||
"PCSt5T": "Postavke",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Vaša mnemotehnička fraza",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Nepoznati header file-a: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Spremni ste!",
|
||||
"zonsdq": "Neuspješno učitavanje LNURL usluge",
|
||||
"zvCDao": "Automatski prikaži najnovije bilješke",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Nyilvános",
|
||||
"/RD0e2": "A Nostr digitális aláírásokat használ, amivel megmásíthatatlan bejegyzéseket lehet létrehozni. Ezeket bárhány másolatban a különböző csomópontokra szétszórhatóak, ezzel biztosítva a tartalom redundanciáját.",
|
||||
"/Xf4UW": "Anonimizált használati adatok küldése",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Legyen a fiókod könnyebben megtalálható és megosztható",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Továbbiak betöltése",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Rendszer (Alapértelmezett)",
|
||||
"P7nJT9": "Összesen ma (UTC): {amount} sat",
|
||||
"PCSt5T": "Preferenciák",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Fiók helyreállító (mnemonikus) szavaid",
|
||||
"PaN7t3": "Előnézet a(z) {site}-n",
|
||||
"PamNxw": "Ismeretlen fejléc: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Készen vagy!",
|
||||
"zonsdq": "Az LNURL szolgáltatás betöltése nem sikerült",
|
||||
"zvCDao": "Automatikusan a legfrissebb bejegyzéseket mutassa",
|
||||
"zwb6LR": "<b>Pénzverde:</b> {url}"
|
||||
"zwb6LR": "<b>Pénzverde:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Publik",
|
||||
"/RD0e2": "Nostr menggunakan teknologi tanda tangan digital untuk menyediakan catatan anti pengerusakan yang dapat dengan aman direplikasi ke banyak relai untuk menyediakan penyimpanan konten Anda yang berulang.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Jadikan profil Anda lebih mudah untuk ditemukan dan dibagikan",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Sistem (Bawaan)",
|
||||
"P7nJT9": "Total hari ini (UTC): {amount} sat",
|
||||
"PCSt5T": "Preferensi",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Your mnemonic phrase",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Unknown file header: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Anda siap!",
|
||||
"zonsdq": "Gagal memuat layanan LNURL",
|
||||
"zvCDao": "Tampilkan catatan terbaru secara otomatis",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Pubblica",
|
||||
"/RD0e2": "Nostr utilizza la tecnologia della firma digitale per fornire note a prova di manomissione che possono essere replicate in modo sicuro su molti relè per fornire l'archiviazione ridondante dei tuoi contenuti.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Rendi il tuo profilo più facile da trovare e condividere",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Sistema (Predefinito)",
|
||||
"P7nJT9": "Totale oggi (UTC): {amount} sat",
|
||||
"PCSt5T": "Preferenze",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "La tua frase mnemonica",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Intestazione del file sconosciuta: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Tutto fatto!",
|
||||
"zonsdq": "Caricamento del servizio LNURL fallito",
|
||||
"zvCDao": "Visualizza automaticamente le ultime note",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "公開",
|
||||
"/RD0e2": "Nostrはデジタル署名技術を使って投稿の改竄防止を図り、多数のリレーに安全に複製してコンテンツの冗長ストレージを提供しています。",
|
||||
"/Xf4UW": "匿名で利用状況を送信します",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "プロフィールを見つけやすく、共有しやすくなる",
|
||||
"/n5KSF": "{n}ミリ秒",
|
||||
"00LcfG": "さらに読み込む",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "システム (デフォルト)",
|
||||
"P7nJT9": "本日の合計額 (UTC): {amount} sats",
|
||||
"PCSt5T": "ユーザー設定",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "ニーモニックフレーズ",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "不明なファイルヘッダー: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "準備完了!",
|
||||
"zonsdq": "LNURLサービスの読み込みに失敗しました",
|
||||
"zvCDao": "最新の記事を自動で表示する",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Openbaar",
|
||||
"/RD0e2": "Nostr maakt gebruik van digitale handtekeningentechnologie om manipulatiebeveiligde notes te realiseren. Zo kunnen de notes consistent naar veel relays tegelijk worden verzonden en wordt overbodige opslag voorkomen.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Maak je profiel gemakkelijker om te vinden en te delen",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Meer laden",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Systeem (Standaard)",
|
||||
"P7nJT9": "Totaal vandaag (UTC): {amount} sats",
|
||||
"PCSt5T": "Voorkeuren",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Uw wachtwoord om te onthouden",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Onbekende bestandsheader: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Je bent er klaar voor!",
|
||||
"zonsdq": "Laden van LNURL service mislukt",
|
||||
"zvCDao": "Recente notities automatisch weergeven",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Público",
|
||||
"/RD0e2": "Nostr usa tecnologia de assinatura digital para prover notas à prova de adulteração que podem ser repostadas em diferentes relays para que haja armazenamento redundante de seu conteúdo.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Faça seu perfil seja mais fácil de achar e compartilhar",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Carregar mais",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Sistema (Padrão)",
|
||||
"P7nJT9": "Total hoje (UTC): {amount} sats",
|
||||
"PCSt5T": "Preferências",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "A sua senha mnemônica",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Cabeçalho de arquivo desconhecido: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Você está pronto!",
|
||||
"zonsdq": "Falha ao carregar o serviço LNURL",
|
||||
"zvCDao": "Mostrar automaticamente as últimas notas",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Публичный",
|
||||
"/RD0e2": "Nostr использует технологию цифровой подписи для создания защищенных от несанкционированного доступа заметок, которые можно безопасно копировать на множество релеев, обеспечивая резервное хранение Вашего контента.",
|
||||
"/Xf4UW": "Отправлять показатели анонимного использования",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Упростить поиск и распространение вашего профиля в Nostr",
|
||||
"/n5KSF": "{n} мс",
|
||||
"00LcfG": "Загрузить больше",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Системный (по умолчанию)",
|
||||
"P7nJT9": "Всего сегодня (UTC): {amount} сат",
|
||||
"PCSt5T": "Настройки",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Ваша мнемоническая фраза",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Неизвестный заголовок файла: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Готово!",
|
||||
"zonsdq": "Не удалось загрузить службу LNURL",
|
||||
"zvCDao": "Автоматически показывать новые заметки",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Publik",
|
||||
"/RD0e2": "Nostr använder digital signatur teknik för att ge manipuleringssäkra anteckningar som säkert kan replikeras till många reläer för att ge redundant lagring av ditt innehåll.",
|
||||
"/Xf4UW": "Skicka anonyma användarvärden",
|
||||
"/clOBU": "Veckovis",
|
||||
"/d6vEc": "Gör din profil enklare att hitta och dela",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Ladda fler",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "System (standard)",
|
||||
"P7nJT9": "Totalt idag (UTC): {amount} sats",
|
||||
"PCSt5T": "Inställningar",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Din mnemoniska lösenfras",
|
||||
"PaN7t3": "Förhandsgranska på {site}",
|
||||
"PamNxw": "Okänd filrubrik: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Du är redo!",
|
||||
"zonsdq": "Det gick inte att ladda LNURL-tjänsten",
|
||||
"zvCDao": "Visa automatiskt de senaste anteckningarna",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "Umma",
|
||||
"/RD0e2": "Nostr hutumia teknolojia ya sahihi ya dijiti kutoa madokezo ya uthibitisho ambayo yanaweza kuigwa kwa usalama kwenye relay nyingi ili kutoa uhifadhi mwingi wa maudhui yako.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "Rahisisha wasifu wako kupata na kushiriki",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Pakia zaidi",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "Mfumo (Chaguo-msingi)",
|
||||
"P7nJT9": "Jumla ya leo (UTC): {amount} sats",
|
||||
"PCSt5T": "Preferences",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Maneno yako ya mnemonic",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "Kijajuu cha faili kisichojulikana: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "Uko tayari!",
|
||||
"zonsdq": "Imeshindwa kupakia huduma ya LNURL",
|
||||
"zvCDao": "Onyesha madokezo mapya kiotomatiki",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "பொது",
|
||||
"/RD0e2": "சேதப்படுத்த முடியாத குறிப்புகளை வழங்க, நாஸ்டர் டிஜிட்டல் கையொப்ப தொழில் நுட்பத்தைப் பயன் படுத்துகிறது. இதனால் பல ரிலேகளில் குறிப்புகள் பிரதியெடுக்கப் பட்டு, குறிப்பின் உள்ளடக்கம் கூடுதல் சேமிப்பு அடைகிறது.",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "உங்கள் சுயவிவரத்தை கண்டறிவதையும் பகிர்வதையும் எளிதாக்குங்கள்",
|
||||
"/n5KSF": "{n} மில்லி வினாடிகள்",
|
||||
"00LcfG": "மேலும் காண்க",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "கணினி (இயல்புநிலை)",
|
||||
"P7nJT9": "இன்றைய (UTC) மொத்தம்: {amount} சாட்கள்",
|
||||
"PCSt5T": "விருப்பங்கள்",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "உங்கள் நினைவூட்டும் சொற்றொடர்",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "தெரியாத கோப்புத் தலைப்பு: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "நீங்கள் தயார்!",
|
||||
"zonsdq": "LNURL சேவையை ஏற்றுவதில் தோல்வி",
|
||||
"zvCDao": "சமீபத்திய குறிப்புகளைத் தானாகக் காட்டு",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "สาธารณะ",
|
||||
"/RD0e2": "Nostr ใช้เทคโนโลยีลายเซ็นดิจิทัลเพื่อแสดงบันทึกหลักฐานการปลอมแปลง ซึ่งสามารถทำซ้ำได้อย่างปลอดภัยไปยังรีเลย์จำนวนมาก เพื่อกระจายการจัดเก็บเนื้อหาที่ซ้ำซ้อนของคุณ",
|
||||
"/Xf4UW": "Send anonymous usage metrics",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "ทำให้ค้นหาและแบ่งปันโปรไฟล์ได้ง่ายขึ้น",
|
||||
"/n5KSF": "{n} ms",
|
||||
"00LcfG": "Load more",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "ระบบ (ค่าเริ่มต้น)",
|
||||
"P7nJT9": "ยอดรวมวันนี้ (UTC): {amount} sats",
|
||||
"PCSt5T": "การตั้งค่า",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "Mnemonic phrase ของคุณ",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "ส่วนต้นของไฟล์ที่ไม่รู้จัก: {name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "คุณพร้อมแล้ว!",
|
||||
"zonsdq": "ไม่สามารถโหลดบริการ LNURL",
|
||||
"zvCDao": "โชว์โน้ตแบบย่ออัตโนมัติ",
|
||||
"zwb6LR": "<b>Mint:</b> {url}"
|
||||
"zwb6LR": "<b>Mint:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "公开",
|
||||
"/RD0e2": "Nostr 利用数字签名技术实现防篡改的笔记并可令其安全的复制到大量中继节点,给你的内容作为冗余存储。",
|
||||
"/Xf4UW": "发送匿名使用资料",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "使你的帐号可更方便地被找到及分享",
|
||||
"/n5KSF": "{n} 毫秒",
|
||||
"00LcfG": "加载更多",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "系统(默认)",
|
||||
"P7nJT9": "今天总计 (UTC):{amount} 聪",
|
||||
"PCSt5T": "选项",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "你的助记词句",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "未知文件标头:{name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "你已准备就绪!",
|
||||
"zonsdq": "加载 LNURL 服务失败",
|
||||
"zvCDao": "自动显示最新笔记",
|
||||
"zwb6LR": "<b>铸币厂:</b> {url}"
|
||||
"zwb6LR": "<b>铸币厂:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"/PCavi": "公開",
|
||||
"/RD0e2": "Nostr 利用數字簽名技術實現防篡改的筆記並可令其安全的複製到大量的中繼節點,給你的內容作為冗余存儲。",
|
||||
"/Xf4UW": "傳送匿名使用資料",
|
||||
"/clOBU": "Weekly",
|
||||
"/d6vEc": "使你的帳號可更方便地被找到及分享",
|
||||
"/n5KSF": "{n} 毫秒",
|
||||
"00LcfG": "加載更多",
|
||||
@ -231,6 +232,7 @@
|
||||
"P7FD0F": "系統(默認)",
|
||||
"P7nJT9": "今天總計(UTC):{amount} 聰",
|
||||
"PCSt5T": "選項",
|
||||
"PJeJFc": "Summary",
|
||||
"PLSbmL": "你的助記詞句",
|
||||
"PaN7t3": "Preview on {site}",
|
||||
"PamNxw": "未知文件標頭:{name}",
|
||||
@ -493,5 +495,6 @@
|
||||
"zjJZBd": "你已經準備就緒!",
|
||||
"zonsdq": "加載 LNURL 服務失敗",
|
||||
"zvCDao": "自動顯示最新筆記",
|
||||
"zwb6LR": "<b>鑄幣廠:</b> {url}"
|
||||
"zwb6LR": "<b>鑄幣廠:</b> {url}",
|
||||
"zxvhnE": "Daily"
|
||||
}
|
||||
|
@ -88,11 +88,7 @@ const config = {
|
||||
})
|
||||
: false,
|
||||
new DefinePlugin({
|
||||
"process.env.APP_NAME": JSON.stringify(appConfig.get("appName")),
|
||||
"process.env.APP_NAME_CAPITALIZED": JSON.stringify(appConfig.get("appNameCapitalized")),
|
||||
"process.env.NIP05_DOMAIN": JSON.stringify(appConfig.get("nip05Domain")),
|
||||
"process.env.HTTP_CACHE": JSON.stringify(appConfig.get("httpCache")),
|
||||
"process.env.ANIMAL_NAME_PLACEHOLDERS": JSON.stringify(appConfig.get("animalNamePlaceholders")),
|
||||
CONFIG: JSON.stringify(appConfig),
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
|
@ -11,24 +11,6 @@ export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
|
||||
h => {
|
||||
if (pubKey) {
|
||||
system.ProfileLoader.TrackMetadata(pubKey);
|
||||
if (process.env.HTTP_CACHE && !system.ProfileLoader.Cache.getFromCache(pubKey)) {
|
||||
fetch(`${process.env.HTTP_CACHE}/profile/${pubKey}`)
|
||||
.then(async r => {
|
||||
if (r.ok) {
|
||||
try {
|
||||
const data = await r.json();
|
||||
if (data) {
|
||||
system.ProfileLoader.onProfileEvent(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
const release = system.ProfileLoader.Cache.hook(h, pubKey);
|
||||
return () => {
|
||||
|
25
packages/system-web/package.json
Normal file
25
packages/system-web/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@snort/system-web",
|
||||
"version": "1.0.0",
|
||||
"description": "Web based components @snort/system",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"repository": "https://git.v0l.io/Kieran/snort",
|
||||
"author": "Kieran",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@snort/shared": "^1.0.6",
|
||||
"@snort/system": "^1.0.21",
|
||||
"dexie": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { DexieLike, DexieTableLike } from "@snort/shared";
|
||||
import { MetadataCache, RelayMetrics, UsersRelays } from ".";
|
||||
import { NostrEvent } from "../nostr";
|
||||
import { NostrEvent, MetadataCache, RelayMetrics, UsersRelays } from "@snort/system";
|
||||
import Dexie, { Table } from "dexie";
|
||||
|
||||
const NAME = "snort-system";
|
||||
const VERSION = 2;
|
||||
@ -12,13 +11,12 @@ const STORES = {
|
||||
events: "++id, pubkey, created_at",
|
||||
};
|
||||
|
||||
export class SnortSystemDb extends DexieLike {
|
||||
export class SnortSystemDb extends Dexie {
|
||||
ready = false;
|
||||
users!: DexieTableLike<MetadataCache>;
|
||||
relayMetrics!: DexieTableLike<RelayMetrics>;
|
||||
userRelays!: DexieTableLike<UsersRelays>;
|
||||
events!: DexieTableLike<NostrEvent>;
|
||||
dms!: DexieTableLike<NostrEvent>;
|
||||
users!: Table<MetadataCache>;
|
||||
relayMetrics!: Table<RelayMetrics>;
|
||||
userRelays!: Table<UsersRelays>;
|
||||
events!: Table<NostrEvent>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
18
packages/system-web/tsconfig.json
Normal file
18
packages/system-web/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitOverride": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSourceMap": true,
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"files": ["src/index.ts"]
|
||||
}
|
7
packages/system/src/cache/events.ts
vendored
7
packages/system/src/cache/events.ts
vendored
@ -1,10 +1,9 @@
|
||||
import { NostrEvent } from "nostr";
|
||||
import { db } from ".";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { DexieTableLike, FeedCache } from "@snort/shared";
|
||||
|
||||
export class EventsCache extends FeedCache<NostrEvent> {
|
||||
constructor() {
|
||||
super("EventsCache", db.events);
|
||||
constructor(table?: DexieTableLike<NostrEvent>) {
|
||||
super("EventsCache", table);
|
||||
}
|
||||
|
||||
key(of: NostrEvent): string {
|
||||
|
14
packages/system/src/cache/index.ts
vendored
14
packages/system/src/cache/index.ts
vendored
@ -1,8 +1,5 @@
|
||||
import { FullRelaySettings, HexKey, NostrEvent, UserMetadata } from "..";
|
||||
import { hexToBech32, unixNowMs } from "@snort/shared";
|
||||
import { SnortSystemDb } from "./db";
|
||||
|
||||
export const db = new SnortSystemDb();
|
||||
import { hexToBech32, unixNowMs, DexieTableLike } from "@snort/shared";
|
||||
|
||||
export interface MetadataCache extends UserMetadata {
|
||||
/**
|
||||
@ -71,3 +68,12 @@ export function mapEventToProfile(ev: NostrEvent) {
|
||||
console.error("Failed to parse JSON", ev, e);
|
||||
}
|
||||
}
|
||||
|
||||
export interface SnortSystemDb {
|
||||
users: DexieTableLike<MetadataCache>;
|
||||
relayMetrics: DexieTableLike<RelayMetrics>;
|
||||
userRelays: DexieTableLike<UsersRelays>;
|
||||
events: DexieTableLike<NostrEvent>;
|
||||
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
8
packages/system/src/cache/relay-metric.ts
vendored
8
packages/system/src/cache/relay-metric.ts
vendored
@ -1,9 +1,9 @@
|
||||
import { db, RelayMetrics } from ".";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { RelayMetrics } from ".";
|
||||
import { DexieTableLike, FeedCache } from "@snort/shared";
|
||||
|
||||
export class RelayMetricCache extends FeedCache<RelayMetrics> {
|
||||
constructor() {
|
||||
super("RelayMetrics", db.relayMetrics);
|
||||
constructor(table?: DexieTableLike<RelayMetrics>) {
|
||||
super("RelayMetrics", table);
|
||||
}
|
||||
|
||||
key(of: RelayMetrics): string {
|
||||
|
12
packages/system/src/cache/user-metadata.ts
vendored
12
packages/system/src/cache/user-metadata.ts
vendored
@ -1,12 +1,12 @@
|
||||
import { db, MetadataCache } from ".";
|
||||
import { fetchNip05Pubkey, FeedCache, LNURL } from "@snort/shared";
|
||||
import { MetadataCache } from ".";
|
||||
import { fetchNip05Pubkey, FeedCache, LNURL, DexieTableLike } from "@snort/shared";
|
||||
|
||||
export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
#zapperQueue: Array<{ pubkey: string; lnurl: string }> = [];
|
||||
#nip5Queue: Array<{ pubkey: string; nip05: string }> = [];
|
||||
|
||||
constructor() {
|
||||
super("UserCache", db.users);
|
||||
constructor(table?: DexieTableLike<MetadataCache>) {
|
||||
super("UserCache", table);
|
||||
this.#processZapperQueue();
|
||||
this.#processNip5Queue();
|
||||
}
|
||||
@ -24,10 +24,10 @@ export class UserProfileCache extends FeedCache<MetadataCache> {
|
||||
}
|
||||
|
||||
async search(q: string): Promise<Array<MetadataCache>> {
|
||||
if (db.ready) {
|
||||
if (this.table) {
|
||||
// on-disk cache will always have more data
|
||||
return (
|
||||
await db.users
|
||||
await this.table
|
||||
.where("npub")
|
||||
.startsWithIgnoreCase(q)
|
||||
.or("name")
|
||||
|
8
packages/system/src/cache/user-relays.ts
vendored
8
packages/system/src/cache/user-relays.ts
vendored
@ -1,9 +1,9 @@
|
||||
import { db, UsersRelays } from ".";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { UsersRelays } from ".";
|
||||
import { DexieTableLike, FeedCache } from "@snort/shared";
|
||||
|
||||
export class UserRelaysCache extends FeedCache<UsersRelays> {
|
||||
constructor() {
|
||||
super("UserRelays", db.userRelays);
|
||||
constructor(table?: DexieTableLike<UsersRelays>) {
|
||||
super("UserRelays", table);
|
||||
}
|
||||
|
||||
key(of: UsersRelays): string {
|
||||
|
@ -16,8 +16,8 @@ import {
|
||||
UserProfileCache,
|
||||
UserRelaysCache,
|
||||
RelayMetricCache,
|
||||
db,
|
||||
UsersRelays,
|
||||
SnortSystemDb,
|
||||
} from ".";
|
||||
import { EventsCache } from "./cache/events";
|
||||
import { RelayCache } from "./gossip-model";
|
||||
@ -87,19 +87,21 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
||||
relayMetrics?: FeedCache<RelayMetrics>;
|
||||
eventsCache?: FeedCache<NostrEvent>;
|
||||
queryOptimizer?: QueryOptimizer;
|
||||
db?: SnortSystemDb;
|
||||
}) {
|
||||
super();
|
||||
this.#handleAuth = props.authHandler;
|
||||
this.#relayCache = props.relayCache ?? new UserRelaysCache();
|
||||
this.#profileCache = props.profileCache ?? new UserProfileCache();
|
||||
this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache();
|
||||
this.#eventsCache = props.eventsCache ?? new EventsCache();
|
||||
this.#relayCache = props.relayCache ?? new UserRelaysCache(props.db?.userRelays);
|
||||
this.#profileCache = props.profileCache ?? new UserProfileCache(props.db?.users);
|
||||
this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache(props.db?.relayMetrics);
|
||||
this.#eventsCache = props.eventsCache ?? new EventsCache(props.db?.events);
|
||||
this.#queryOptimizer = props.queryOptimizer ?? DefaultQueryOptimizer;
|
||||
|
||||
this.#profileLoader = new ProfileLoaderService(this, this.#profileCache);
|
||||
this.#relayMetrics = new RelayMetricHandler(this.#relayMetricsCache);
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
HandleAuth?: AuthHandler | undefined;
|
||||
|
||||
get ProfileLoader() {
|
||||
@ -122,7 +124,6 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
|
||||
* Setup caches
|
||||
*/
|
||||
async Init() {
|
||||
db.ready = await db.isAvailable();
|
||||
const t = [
|
||||
this.#relayCache.preload(),
|
||||
this.#profileCache.preload(),
|
||||
|
@ -1,8 +1,9 @@
|
||||
import debug from "debug";
|
||||
import { unixNowMs, FeedCache } from "@snort/shared";
|
||||
import { EventKind, HexKey, SystemInterface, TaggedNostrEvent, NoteCollection, RequestBuilder } from ".";
|
||||
import { EventKind, HexKey, SystemInterface, TaggedNostrEvent, RequestBuilder } from ".";
|
||||
import { ProfileCacheExpire } from "./const";
|
||||
import { mapEventToProfile, MetadataCache } from "./cache";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
const MetadataRelays = ["wss://purplepag.es"];
|
||||
|
||||
@ -23,6 +24,11 @@ export class ProfileLoaderService {
|
||||
|
||||
readonly #log = debug("ProfileCache");
|
||||
|
||||
/**
|
||||
* Custom loader function for fetching profiles from alternative sources
|
||||
*/
|
||||
loaderFn?: (pubkeys: Array<string>) => Promise<Array<MetadataCache>>;
|
||||
|
||||
constructor(system: SystemInterface, cache: FeedCache<MetadataCache>) {
|
||||
this.#system = system;
|
||||
this.#cache = cache;
|
||||
@ -92,50 +98,8 @@ export class ProfileLoaderService {
|
||||
if (missing.size > 0) {
|
||||
this.#log("Wants profiles: %d missing, %d expired", missingFromCache.length, expired.length);
|
||||
|
||||
const sub = new RequestBuilder("profiles");
|
||||
sub
|
||||
.withOptions({
|
||||
skipDiff: true,
|
||||
})
|
||||
.withFilter()
|
||||
.kinds([EventKind.SetMetadata])
|
||||
.authors([...missing]);
|
||||
const results = await this.#loadProfiles([...missing]);
|
||||
|
||||
if (this.#missingLastRun.size > 0) {
|
||||
const fMissing = sub
|
||||
.withFilter()
|
||||
.kinds([EventKind.SetMetadata])
|
||||
.authors([...this.#missingLastRun]);
|
||||
MetadataRelays.forEach(r => fMissing.relay(r));
|
||||
}
|
||||
const newProfiles = new Set<string>();
|
||||
const q = this.#system.Query<NoteCollection>(NoteCollection, sub);
|
||||
const feed = (q?.feed as NoteCollection) ?? new NoteCollection();
|
||||
// never release this callback, it will stop firing anyway after eose
|
||||
const releaseOnEvent = feed.onEvent(async e => {
|
||||
for (const pe of e) {
|
||||
newProfiles.add(pe.id);
|
||||
await this.onProfileEvent(pe);
|
||||
}
|
||||
});
|
||||
const results = await new Promise<Readonly<Array<TaggedNostrEvent>>>(resolve => {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
const release = feed.hook(() => {
|
||||
if (!feed.loading) {
|
||||
clearTimeout(timeout);
|
||||
resolve(feed.getSnapshotData() ?? []);
|
||||
this.#log("Profiles finished: %s", sub.id);
|
||||
release();
|
||||
}
|
||||
});
|
||||
timeout = setTimeout(() => {
|
||||
release();
|
||||
resolve(feed.getSnapshotData() ?? []);
|
||||
this.#log("Profiles timeout: %s", sub.id);
|
||||
}, 5_000);
|
||||
});
|
||||
|
||||
releaseOnEvent();
|
||||
const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a));
|
||||
this.#missingLastRun = new Set(couldNotFetch);
|
||||
if (couldNotFetch.length > 0) {
|
||||
@ -150,12 +114,43 @@ export class ProfileLoaderService {
|
||||
await Promise.all(empty);
|
||||
}
|
||||
|
||||
// When we fetch an expired profile and its the same as what we already have
|
||||
/* When we fetch an expired profile and its the same as what we already have
|
||||
// onEvent is not fired and the loaded timestamp never gets updated
|
||||
const expiredSame = results.filter(a => !newProfiles.has(a.id) && expired.includes(a.pubkey));
|
||||
await Promise.all(expiredSame.map(v => this.onProfileEvent(v)));
|
||||
await Promise.all(expiredSame.map(v => this.onProfileEvent(v)));*/
|
||||
}
|
||||
|
||||
setTimeout(() => this.#FetchMetadata(), 500);
|
||||
}
|
||||
|
||||
async #loadProfiles(missing: Array<string>) {
|
||||
if (this.loaderFn) {
|
||||
const results = await this.loaderFn(missing);
|
||||
await Promise.all(results.map(a => this.#cache.update(a)));
|
||||
return results;
|
||||
} else {
|
||||
const sub = new RequestBuilder(`profiles-${uuid()}`);
|
||||
sub
|
||||
.withOptions({
|
||||
skipDiff: true,
|
||||
})
|
||||
.withFilter()
|
||||
.kinds([EventKind.SetMetadata])
|
||||
.authors(missing);
|
||||
|
||||
if (this.#missingLastRun.size > 0) {
|
||||
const fMissing = sub
|
||||
.withFilter()
|
||||
.kinds([EventKind.SetMetadata])
|
||||
.authors([...this.#missingLastRun]);
|
||||
MetadataRelays.forEach(r => fMissing.relay(r));
|
||||
}
|
||||
const results = (await this.#system.Fetch(sub, async e => {
|
||||
for (const pe of e) {
|
||||
await this.onProfileEvent(pe);
|
||||
}
|
||||
})) as ReadonlyArray<TaggedNostrEvent>;
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ export function transformText(body: string, tags: Array<Array<string>>) {
|
||||
fragments = fragments
|
||||
.map(a => {
|
||||
if (typeof a === "string") {
|
||||
if (a.trim().length > 0) {
|
||||
if (a.length > 0) {
|
||||
return { type: "text", content: a } as ParsedFragment;
|
||||
}
|
||||
} else {
|
||||
|
364
yarn.lock
364
yarn.lock
@ -1376,6 +1376,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.1.2":
|
||||
version: 7.23.1
|
||||
resolution: "@babel/runtime@npm:7.23.1"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.14.0
|
||||
checksum: 0cd0d43e6e7dc7f9152fda8c8312b08321cda2f56ef53d6c22ebdd773abdc6f5d0a69008de90aa41908d00e2c1facb24715ff121274e689305c858355ff02c70
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.8.4":
|
||||
version: 7.22.11
|
||||
resolution: "@babel/runtime@npm:7.22.11"
|
||||
@ -2692,6 +2701,7 @@ __metadata:
|
||||
"@snort/system": "workspace:*"
|
||||
"@snort/system-react": "workspace:*"
|
||||
"@snort/system-wasm": "workspace:*"
|
||||
"@snort/system-web": "workspace:*"
|
||||
"@szhsin/react-menu": ^3.3.1
|
||||
"@types/debug": ^4.1.8
|
||||
"@types/jest": ^29.5.1
|
||||
@ -2704,6 +2714,7 @@ __metadata:
|
||||
"@types/webtorrent": ^0.109.3
|
||||
"@typescript-eslint/eslint-plugin": ^6.1.0
|
||||
"@typescript-eslint/parser": ^6.1.0
|
||||
"@uidotdev/usehooks": ^2.3.1
|
||||
"@void-cat/api": ^1.0.4
|
||||
"@webbtc/webln-types": ^1.0.10
|
||||
"@webpack-cli/generators": ^3.0.4
|
||||
@ -2735,6 +2746,7 @@ __metadata:
|
||||
react-router-dom: ^6.5.0
|
||||
react-textarea-autosize: ^8.4.0
|
||||
react-twitter-embed: ^4.0.4
|
||||
recharts: ^2.8.0
|
||||
source-map-loader: ^4.0.1
|
||||
terser-webpack-plugin: ^5.3.9
|
||||
tinybench: ^2.5.1
|
||||
@ -2799,6 +2811,17 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@snort/system-web@workspace:*, @snort/system-web@workspace:packages/system-web":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@snort/system-web@workspace:packages/system-web"
|
||||
dependencies:
|
||||
"@snort/shared": ^1.0.6
|
||||
"@snort/system": ^1.0.21
|
||||
dexie: ^3.2.4
|
||||
typescript: ^5.2.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@snort/system@^1.0.21, @snort/system@workspace:*, @snort/system@workspace:packages/system":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@snort/system@workspace:packages/system"
|
||||
@ -3161,6 +3184,75 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-array@npm:^3.0.3":
|
||||
version: 3.0.8
|
||||
resolution: "@types/d3-array@npm:3.0.8"
|
||||
checksum: d5a678f1dc3af05bc6beb675d59a11d9b2ad4ea59fb5b6c2b99980fec947d89a9562f3ac3a8d192a4f38152d3a4b9ee9cf4e2a30788eaacaed5de4a6da514e10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-color@npm:*":
|
||||
version: 3.1.1
|
||||
resolution: "@types/d3-color@npm:3.1.1"
|
||||
checksum: 1fa67a6d11386c2727c942ab0ddffaca2289ba01d2f3cd0723afc78c291e9515dbdc6de082466d9e9c360d7c67ddbf313707456c0daa9aa14acb2d48cb3bcabb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-ease@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@types/d3-ease@npm:3.0.0"
|
||||
checksum: 1be7c993643b5a08332e0ee146375a3845545d8deb423db5d152e0b061524385d2345ceccf968f75f605247b940dd3f9a144335fee2e7d935cddaf187afb7095
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-interpolate@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "@types/d3-interpolate@npm:3.0.2"
|
||||
dependencies:
|
||||
"@types/d3-color": "*"
|
||||
checksum: 86a1c4853c70663cba970d5c57dca995f604a70684b17bc5ff3ba83ce4e2c13f0105af29bb383ee70c4ccb1920c0dd4aeb352ae8721864d4a503a110260b9b13
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-path@npm:*":
|
||||
version: 3.0.0
|
||||
resolution: "@types/d3-path@npm:3.0.0"
|
||||
checksum: af7f45ea912cddd794c03384baba856f11e1f9b57a49d05a66a61968dafaeb86e0e42394883118b9b8ccadce21a5f25b1f9a88ad05235e1dc6d24c3e34a379ff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-scale@npm:^4.0.2":
|
||||
version: 4.0.5
|
||||
resolution: "@types/d3-scale@npm:4.0.5"
|
||||
dependencies:
|
||||
"@types/d3-time": "*"
|
||||
checksum: f462a3f2ec8767bb6762953ed65087b4037d9f8c57c84b1ffc62d55b7633975611e053c2f36cef063bf123196fbb5741b257760b2a745ede9544851f7d150d60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-shape@npm:^3.1.0":
|
||||
version: 3.1.3
|
||||
resolution: "@types/d3-shape@npm:3.1.3"
|
||||
dependencies:
|
||||
"@types/d3-path": "*"
|
||||
checksum: ad17781ab4ce4b796954b86de7e14566c731726d39a1db7d73eaf50668a71e996d715450a0ff9f2720755e1b8643c3e88d47d45101a75c9d4ddbef51a636f6a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-time@npm:*, @types/d3-time@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "@types/d3-time@npm:3.0.1"
|
||||
checksum: 32b0c4d33574df167717f37d5d69f60fa1aeebb0218823239734a48e6a33024a7f5aadd079e94d833b42bfc0c3e2d9fa7d7ac93f75981f59ef2a46838d008a61
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-timer@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@types/d3-timer@npm:3.0.0"
|
||||
checksum: 1ec86b3808de6ecfa93cfdf34254761069658af0cc1d9540e8353dbcba161cdf1296a0724187bd17433b2ff16563115fd20b85fc89d5e809ff28f9b1ab134b42
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/debug@npm:^4.1.8":
|
||||
version: 4.1.8
|
||||
resolution: "@types/debug@npm:4.1.8"
|
||||
@ -3761,6 +3853,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uidotdev/usehooks@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "@uidotdev/usehooks@npm:2.3.1"
|
||||
peerDependencies:
|
||||
react: ">=18.0.0"
|
||||
react-dom: ">=18.0.0"
|
||||
checksum: a1339b91bdb4176f59fc2dd8273065fccacb17749b7022879982ff874bda8e4e54a3f8d74f126e6224164fb2ad422f1cc40dac8705467960df525b207fcd3a79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@void-cat/api@npm:^1.0.4":
|
||||
version: 1.0.7
|
||||
resolution: "@void-cat/api@npm:1.0.7"
|
||||
@ -5010,6 +5112,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"classnames@npm:^2.2.5":
|
||||
version: 2.3.2
|
||||
resolution: "classnames@npm:2.3.2"
|
||||
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clean-css@npm:^5.2.2":
|
||||
version: 5.3.2
|
||||
resolution: "clean-css@npm:5.3.2"
|
||||
@ -5553,6 +5662,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-unit-converter@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "css-unit-converter@npm:1.1.2"
|
||||
checksum: 07888033346a5128f34dbe2f72884c966d24e9f29db24416dcde92860242490617ef9a178ac193a92f730834bbeea026cdc7027701d92ba9bbbe59db7a37eb2a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-what@npm:^6.0.1, css-what@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "css-what@npm:6.1.0"
|
||||
@ -5675,6 +5791,99 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6":
|
||||
version: 3.2.4
|
||||
resolution: "d3-array@npm:3.2.4"
|
||||
dependencies:
|
||||
internmap: 1 - 2
|
||||
checksum: a5976a6d6205f69208478bb44920dd7ce3e788c9dceb86b304dbe401a4bfb42ecc8b04c20facde486e9adcb488b5d1800d49393a3f81a23902b68158e12cddd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-color@npm:1 - 3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-color@npm:3.1.0"
|
||||
checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-ease@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-ease@npm:3.0.1"
|
||||
checksum: 06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-format@npm:1 - 3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-format@npm:3.1.0"
|
||||
checksum: f345ec3b8ad3cab19bff5dead395bd9f5590628eb97a389b1dd89f0b204c7c4fc1d9520f13231c2c7cf14b7c9a8cf10f8ef15bde2befbab41454a569bd706ca2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-interpolate@npm:3.0.1"
|
||||
dependencies:
|
||||
d3-color: 1 - 3
|
||||
checksum: a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-path@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "d3-path@npm:3.1.0"
|
||||
checksum: 2306f1bd9191e1eac895ec13e3064f732a85f243d6e627d242a313f9777756838a2215ea11562f0c7630c7c3b16a19ec1fe0948b1c82f3317fac55882f6ee5d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "d3-scale@npm:4.0.2"
|
||||
dependencies:
|
||||
d3-array: 2.10.0 - 3
|
||||
d3-format: 1 - 3
|
||||
d3-interpolate: 1.2.0 - 3
|
||||
d3-time: 2.1.1 - 3
|
||||
d3-time-format: 2 - 4
|
||||
checksum: a9c770d283162c3bd11477c3d9d485d07f8db2071665f1a4ad23eec3e515e2cefbd369059ec677c9ac849877d1a765494e90e92051d4f21111aa56791c98729e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-shape@npm:^3.1.0":
|
||||
version: 3.2.0
|
||||
resolution: "d3-shape@npm:3.2.0"
|
||||
dependencies:
|
||||
d3-path: ^3.1.0
|
||||
checksum: de2af5fc9a93036a7b68581ca0bfc4aca2d5a328aa7ba7064c11aedd44d24f310c20c40157cb654359d4c15c3ef369f95ee53d71221017276e34172c7b719cfa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-time-format@npm:2 - 4":
|
||||
version: 4.1.0
|
||||
resolution: "d3-time-format@npm:4.1.0"
|
||||
dependencies:
|
||||
d3-time: 1 - 3
|
||||
checksum: 7342bce28355378152bbd4db4e275405439cabba082d9cd01946d40581140481c8328456d91740b0fe513c51ec4a467f4471ffa390c7e0e30ea30e9ec98fcdf4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "d3-time@npm:3.1.0"
|
||||
dependencies:
|
||||
d3-array: 2 - 3
|
||||
checksum: 613b435352a78d9f31b7f68540788186d8c331b63feca60ad21c88e9db1989fe888f97f242322ebd6365e45ec3fb206a4324cd4ca0dfffa1d9b5feb856ba00a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-timer@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-timer@npm:3.0.1"
|
||||
checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dargs@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "dargs@npm:7.0.0"
|
||||
@ -5728,6 +5937,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decimal.js-light@npm:^2.4.1":
|
||||
version: 2.5.1
|
||||
resolution: "decimal.js-light@npm:2.5.1"
|
||||
checksum: f5a2c7eac1c4541c8ab8a5c8abea64fc1761cefc7794bd5f8afd57a8a78d1b51785e0c4e4f85f4895a043eaa90ddca1edc3981d1263eb6ddce60f32bf5fe66c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decimal.js@npm:^10.4.2":
|
||||
version: 10.4.3
|
||||
resolution: "decimal.js@npm:10.4.3"
|
||||
@ -5947,6 +6163,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-helpers@npm:^3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "dom-helpers@npm:3.4.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.1.2
|
||||
checksum: 58d9f1c4a96daf77eddc63ae1236b826e1cddd6db66bbf39b18d7e21896d99365b376593352d52a60969d67fa4a8dbef26adc1439fa2c1b355efa37cacbaf637
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-serializer@npm:^1.0.1":
|
||||
version: 1.4.1
|
||||
resolution: "dom-serializer@npm:1.4.1"
|
||||
@ -6490,7 +6715,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4":
|
||||
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1, eventemitter3@npm:^4.0.4":
|
||||
version: 4.0.7
|
||||
resolution: "eventemitter3@npm:4.0.7"
|
||||
checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374
|
||||
@ -6605,6 +6830,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-equals@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "fast-equals@npm:5.0.1"
|
||||
checksum: fbb3b6a74f3a0fa930afac151ff7d01639159b4fddd2678b5d50708e0ba38e9ec14602222d10dadb8398187342692c04fbef5a62b1cfcc7942fe03e754e064bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0":
|
||||
version: 3.3.1
|
||||
resolution: "fast-glob@npm:3.3.1"
|
||||
@ -7689,6 +7921,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"internmap@npm:1 - 2":
|
||||
version: 2.0.3
|
||||
resolution: "internmap@npm:2.0.3"
|
||||
checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"interpret@npm:^1.0.0":
|
||||
version: 1.4.0
|
||||
resolution: "interpret@npm:1.4.0"
|
||||
@ -9058,7 +9297,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
|
||||
"lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
|
||||
@ -10879,6 +11118,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-value-parser@npm:^3.3.0":
|
||||
version: 3.3.1
|
||||
resolution: "postcss-value-parser@npm:3.3.1"
|
||||
checksum: 62cd26e1cdbcf2dcc6bcedf3d9b409c9027bc57a367ae20d31dd99da4e206f730689471fd70a2abe866332af83f54dc1fa444c589e2381bf7f8054c46209ce16
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "postcss-value-parser@npm:4.2.0"
|
||||
@ -11040,7 +11286,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@ -11217,7 +11463,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
|
||||
"react-is@npm:^16.10.2, react-is@npm:^16.13.1, react-is@npm:^16.7.0":
|
||||
version: 16.13.1
|
||||
resolution: "react-is@npm:16.13.1"
|
||||
checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f
|
||||
@ -11231,6 +11477,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-lifecycles-compat@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "react-lifecycles-compat@npm:3.0.4"
|
||||
checksum: a904b0fc0a8eeb15a148c9feb7bc17cec7ef96e71188280061fc340043fd6d8ee3ff233381f0e8f95c1cf926210b2c4a31f38182c8f35ac55057e453d6df204f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-resize-detector@npm:^8.0.4":
|
||||
version: 8.1.0
|
||||
resolution: "react-resize-detector@npm:8.1.0"
|
||||
dependencies:
|
||||
lodash: ^4.17.21
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 45e6b87ea7331406bed2a806d0cea98c1467d53a7cfcdf19c2dd55a3460047917d3b175d9cceea6f314b65eb54858cbb981acffd007d67aa16388e517dafb83e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-router-dom@npm:^6.5.0":
|
||||
version: 6.15.0
|
||||
resolution: "react-router-dom@npm:6.15.0"
|
||||
@ -11255,6 +11520,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-smooth@npm:^2.0.2":
|
||||
version: 2.0.4
|
||||
resolution: "react-smooth@npm:2.0.4"
|
||||
dependencies:
|
||||
fast-equals: ^5.0.0
|
||||
react-transition-group: 2.9.0
|
||||
peerDependencies:
|
||||
prop-types: ^15.6.0
|
||||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 21731e2f9ebc9594eae0f0d875526185392a87c00abf013c9769ed642a4077b62c04c1001b2527a196aabafb87af208f6c7107db674538c4bb95c253ed123447
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-textarea-autosize@npm:^8.4.0":
|
||||
version: 8.5.3
|
||||
resolution: "react-textarea-autosize@npm:8.5.3"
|
||||
@ -11268,6 +11547,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-transition-group@npm:2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "react-transition-group@npm:2.9.0"
|
||||
dependencies:
|
||||
dom-helpers: ^3.4.0
|
||||
loose-envify: ^1.4.0
|
||||
prop-types: ^15.6.2
|
||||
react-lifecycles-compat: ^3.0.4
|
||||
peerDependencies:
|
||||
react: ">=15.0.0"
|
||||
react-dom: ">=15.0.0"
|
||||
checksum: d8c9e50aabdc2cfc324e5cdb0ad1c6eecb02e1c0cd007b26d5b30ccf49015e900683dd489348c71fba4055858308d9ba7019e0d37d0e8d37bd46ed098788f670
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-transition-state@npm:^1.1.5":
|
||||
version: 1.1.5
|
||||
resolution: "react-transition-state@npm:1.1.5"
|
||||
@ -11421,6 +11715,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"recharts-scale@npm:^0.4.4":
|
||||
version: 0.4.5
|
||||
resolution: "recharts-scale@npm:0.4.5"
|
||||
dependencies:
|
||||
decimal.js-light: ^2.4.1
|
||||
checksum: e970377190a610e684a32c7461c7684ac3603c2e0ac0020bbba1eea9d099b38138143a8e80bf769bb49c0b7cecf22a2f5c6854885efed2d56f4540d4aa7052bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"recharts@npm:^2.8.0":
|
||||
version: 2.8.0
|
||||
resolution: "recharts@npm:2.8.0"
|
||||
dependencies:
|
||||
classnames: ^2.2.5
|
||||
eventemitter3: ^4.0.1
|
||||
lodash: ^4.17.19
|
||||
react-is: ^16.10.2
|
||||
react-resize-detector: ^8.0.4
|
||||
react-smooth: ^2.0.2
|
||||
recharts-scale: ^0.4.4
|
||||
reduce-css-calc: ^2.1.8
|
||||
victory-vendor: ^36.6.8
|
||||
peerDependencies:
|
||||
prop-types: ^15.6.0
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 4638bd5c6c2af8f5c79de5e13cce0e38f06e0bbb0a3c4df27a9b12632fd72c0a0604c8246f55e830f323dfa84a3da7cb2634c2243bb9c775d899fd71f9d4c87a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rechoir@npm:^0.6.2":
|
||||
version: 0.6.2
|
||||
resolution: "rechoir@npm:0.6.2"
|
||||
@ -11439,6 +11763,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reduce-css-calc@npm:^2.1.8":
|
||||
version: 2.1.8
|
||||
resolution: "reduce-css-calc@npm:2.1.8"
|
||||
dependencies:
|
||||
css-unit-converter: ^1.1.1
|
||||
postcss-value-parser: ^3.3.0
|
||||
checksum: 8fd27c06c4b443b84749a69a8b97d10e6ec7d142b625b41923a8807abb22b9e37e44df14e26cc606a802957be07bdce5e8ee2976a6952a7b438a7727007101e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"regenerate-unicode-properties@npm:^10.1.0":
|
||||
version: 10.1.0
|
||||
resolution: "regenerate-unicode-properties@npm:10.1.0"
|
||||
@ -13401,6 +13735,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"victory-vendor@npm:^36.6.8":
|
||||
version: 36.6.11
|
||||
resolution: "victory-vendor@npm:36.6.11"
|
||||
dependencies:
|
||||
"@types/d3-array": ^3.0.3
|
||||
"@types/d3-ease": ^3.0.0
|
||||
"@types/d3-interpolate": ^3.0.1
|
||||
"@types/d3-scale": ^4.0.2
|
||||
"@types/d3-shape": ^3.1.0
|
||||
"@types/d3-time": ^3.0.0
|
||||
"@types/d3-timer": ^3.0.0
|
||||
d3-array: ^3.1.6
|
||||
d3-ease: ^3.0.1
|
||||
d3-interpolate: ^3.0.1
|
||||
d3-scale: ^4.0.2
|
||||
d3-shape: ^3.1.0
|
||||
d3-time: ^3.0.0
|
||||
d3-timer: ^3.0.1
|
||||
checksum: 55800076dfa6abedf7758840986a302778a904678d4b66fe47d977c48b6f9484276b780871e6e5105b31c1eb936e9f1331ee39afcc2869bf65ceb7d456143172
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vinyl-file@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "vinyl-file@npm:3.0.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user