feat: translations
This commit is contained in:
@ -1,13 +1,17 @@
|
|||||||
import "./BackButton.css";
|
import "./BackButton.css";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import ArrowBack from "Icons/ArrowBack";
|
import ArrowBack from "Icons/ArrowBack";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface BackButtonProps {
|
interface BackButtonProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BackButton = ({ text = "Back", onClick }: BackButtonProps) => {
|
const BackButton = ({ text, onClick }: BackButtonProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const onClickHandler = () => {
|
const onClickHandler = () => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
@ -17,7 +21,7 @@ const BackButton = ({ text = "Back", onClick }: BackButtonProps) => {
|
|||||||
return (
|
return (
|
||||||
<button className="back-button" type="button" onClick={onClickHandler}>
|
<button className="back-button" type="button" onClick={onClickHandler}>
|
||||||
<ArrowBack />
|
<ArrowBack />
|
||||||
{text}
|
{text || formatMessage(messages.Back)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface BlockButtonProps {
|
interface BlockButtonProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
@ -9,11 +12,11 @@ const BlockButton = ({ pubkey }: BlockButtonProps) => {
|
|||||||
const { block, unblock, isBlocked } = useModeration();
|
const { block, unblock, isBlocked } = useModeration();
|
||||||
return isBlocked(pubkey) ? (
|
return isBlocked(pubkey) ? (
|
||||||
<button className="secondary" type="button" onClick={() => unblock(pubkey)}>
|
<button className="secondary" type="button" onClick={() => unblock(pubkey)}>
|
||||||
Unblock
|
<FormattedMessage {...messages.Unblock} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className="secondary" type="button" onClick={() => block(pubkey)}>
|
<button className="secondary" type="button" onClick={() => block(pubkey)}>
|
||||||
Block
|
<FormattedMessage {...messages.Block} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import type { RootState } from "State/Store";
|
import type { RootState } from "State/Store";
|
||||||
@ -9,6 +10,8 @@ import ProfilePreview from "Element/ProfilePreview";
|
|||||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface BlockListProps {
|
interface BlockListProps {
|
||||||
variant: "muted" | "blocked";
|
variant: "muted" | "blocked";
|
||||||
}
|
}
|
||||||
@ -21,7 +24,12 @@ export default function BlockList({ variant }: BlockListProps) {
|
|||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
{variant === "muted" && (
|
{variant === "muted" && (
|
||||||
<>
|
<>
|
||||||
<h4>{muted.length} muted</h4>
|
<h4>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.MuteCount}
|
||||||
|
values={{ n: muted.length }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
{muted.map((a) => {
|
{muted.map((a) => {
|
||||||
return (
|
return (
|
||||||
<ProfilePreview
|
<ProfilePreview
|
||||||
@ -36,7 +44,12 @@ export default function BlockList({ variant }: BlockListProps) {
|
|||||||
)}
|
)}
|
||||||
{variant === "blocked" && (
|
{variant === "blocked" && (
|
||||||
<>
|
<>
|
||||||
<h4>{blocked.length} blocked</h4>
|
<h4>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.BlockCount}
|
||||||
|
values={{ n: blocked.length }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
{blocked.map((a) => {
|
{blocked.map((a) => {
|
||||||
return (
|
return (
|
||||||
<ProfilePreview
|
<ProfilePreview
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "./DM.css";
|
import "./DM.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
@ -12,6 +13,8 @@ import { RootState } from "State/Store";
|
|||||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||||
import { incDmInteraction } from "State/Login";
|
import { incDmInteraction } from "State/Login";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export type DMProps = {
|
export type DMProps = {
|
||||||
data: TaggedRawEvent;
|
data: TaggedRawEvent;
|
||||||
};
|
};
|
||||||
@ -25,6 +28,7 @@ export default function DM(props: DMProps) {
|
|||||||
const [content, setContent] = useState("Loading...");
|
const [content, setContent] = useState("Loading...");
|
||||||
const [decrypted, setDecrypted] = useState(false);
|
const [decrypted, setDecrypted] = useState(false);
|
||||||
const { ref, inView } = useInView();
|
const { ref, inView } = useInView();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const isMe = props.data.pubkey === pubKey;
|
const isMe = props.data.pubkey === pubKey;
|
||||||
const otherPubkey = isMe
|
const otherPubkey = isMe
|
||||||
? pubKey
|
? pubKey
|
||||||
@ -50,7 +54,10 @@ export default function DM(props: DMProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
|
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
|
||||||
<div>
|
<div>
|
||||||
<NoteTime from={props.data.created_at * 1000} fallback={"Just now"} />
|
<NoteTime
|
||||||
|
from={props.data.created_at * 1000}
|
||||||
|
fallback={formatMessage(messages.JustNow)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<Text
|
<Text
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import "./FollowButton.css";
|
import "./FollowButton.css";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faUserMinus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { parseId } from "Util";
|
import { parseId } from "Util";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface FollowButtonProps {
|
export interface FollowButtonProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -35,7 +36,11 @@ export default function FollowButton(props: FollowButtonProps) {
|
|||||||
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
||||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}
|
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}
|
||||||
>
|
>
|
||||||
{isFollowing ? "Unfollow" : "Follow"}
|
{isFollowing ? (
|
||||||
|
<FormattedMessage {...messages.Unfollow} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage {...messages.Follow} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface FollowListBaseProps {
|
export interface FollowListBaseProps {
|
||||||
pubkeys: HexKey[];
|
pubkeys: HexKey[];
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -26,7 +30,7 @@ export default function FollowListBase({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => followAll()}
|
onClick={() => followAll()}
|
||||||
>
|
>
|
||||||
Follow All
|
<FormattedMessage {...messages.FollowAll} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{pubkeys?.map((a) => (
|
{pubkeys?.map((a) => (
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import useFollowersFeed from "Feed/FollowersFeed";
|
import useFollowersFeed from "Feed/FollowersFeed";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import FollowListBase from "Element/FollowListBase";
|
import FollowListBase from "Element/FollowListBase";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface FollowersListProps {
|
export interface FollowersListProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FollowersList({ pubkey }: FollowersListProps) {
|
export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const feed = useFollowersFeed(pubkey);
|
const feed = useFollowersFeed(pubkey);
|
||||||
|
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
@ -18,9 +23,12 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
|||||||
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
||||||
);
|
);
|
||||||
return [...new Set(contactLists?.map((a) => a.pubkey))];
|
return [...new Set(contactLists?.map((a) => a.pubkey))];
|
||||||
}, [feed]);
|
}, [feed, pubkey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FollowListBase pubkeys={pubkeys} title={`${pubkeys?.length} followers`} />
|
<FollowListBase
|
||||||
|
pubkeys={pubkeys}
|
||||||
|
title={formatMessage(messages.FollowerCount, { n: pubkeys?.length })}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import useFollowsFeed from "Feed/FollowsFeed";
|
import useFollowsFeed from "Feed/FollowsFeed";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import FollowListBase from "Element/FollowListBase";
|
import FollowListBase from "Element/FollowListBase";
|
||||||
import { getFollowers } from "Feed/FollowsFeed";
|
import { getFollowers } from "Feed/FollowsFeed";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface FollowsListProps {
|
export interface FollowsListProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FollowsList({ pubkey }: FollowsListProps) {
|
export default function FollowsList({ pubkey }: FollowsListProps) {
|
||||||
const feed = useFollowsFeed(pubkey);
|
const feed = useFollowsFeed(pubkey);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
return getFollowers(feed.store, pubkey);
|
return getFollowers(feed.store, pubkey);
|
||||||
}, [feed]);
|
}, [feed, pubkey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FollowListBase pubkeys={pubkeys} title={`Following ${pubkeys?.length}`} />
|
<FollowListBase
|
||||||
|
pubkeys={pubkeys}
|
||||||
|
title={formatMessage(messages.FollowingCount, { n: pubkeys?.length })}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import "./FollowsYou.css";
|
import "./FollowsYou.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import useFollowsFeed from "Feed/FollowsFeed";
|
import useFollowsFeed from "Feed/FollowsFeed";
|
||||||
import { getFollowers } from "Feed/FollowsFeed";
|
import { getFollowers } from "Feed/FollowsFeed";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface FollowsYouProps {
|
export interface FollowsYouProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const feed = useFollowsFeed(pubkey);
|
const feed = useFollowsFeed(pubkey);
|
||||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(
|
const loginPubKey = useSelector<RootState, HexKey | undefined>(
|
||||||
(s) => s.login.publicKey
|
(s) => s.login.publicKey
|
||||||
@ -18,11 +23,11 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
|||||||
|
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
return getFollowers(feed.store, pubkey);
|
return getFollowers(feed.store, pubkey);
|
||||||
}, [feed]);
|
}, [feed, pubkey]);
|
||||||
|
|
||||||
const followsMe = pubkeys.includes(loginPubKey!) ?? false;
|
const followsMe = pubkeys.includes(loginPubKey!) ?? false;
|
||||||
|
|
||||||
return (
|
return followsMe ? (
|
||||||
<>{followsMe ? <span className="follows-you">follows you</span> : null}</>
|
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import "./Invoice.css";
|
import "./Invoice.css";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import NoteTime from "Element/NoteTime";
|
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import ZapCircle from "Icons/ZapCircle";
|
import ZapCircle from "Icons/ZapCircle";
|
||||||
import useWebln from "Hooks/useWebln";
|
import useWebln from "Hooks/useWebln";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface InvoiceProps {
|
export interface InvoiceProps {
|
||||||
invoice: string;
|
invoice: string;
|
||||||
}
|
}
|
||||||
@ -15,6 +17,7 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
const invoice = props.invoice;
|
const invoice = props.invoice;
|
||||||
const webln = useWebln();
|
const webln = useWebln();
|
||||||
const [showInvoice, setShowInvoice] = useState(false);
|
const [showInvoice, setShowInvoice] = useState(false);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const info = useMemo(() => {
|
const info = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
@ -55,10 +58,12 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
function header() {
|
function header() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Lightning Invoice</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.Invoice} />
|
||||||
|
</h4>
|
||||||
<ZapCircle className="zap-circle" />
|
<ZapCircle className="zap-circle" />
|
||||||
<SendSats
|
<SendSats
|
||||||
title="Pay Invoice"
|
title={formatMessage(messages.PayInvoice)}
|
||||||
invoice={invoice}
|
invoice={invoice}
|
||||||
show={showInvoice}
|
show={showInvoice}
|
||||||
onClose={() => setShowInvoice(false)}
|
onClose={() => setShowInvoice(false)}
|
||||||
@ -102,10 +107,16 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
<div className="invoice-body">
|
<div className="invoice-body">
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
{isPaid ? (
|
{isPaid ? (
|
||||||
<div className="paid">Paid</div>
|
<div className="paid">
|
||||||
|
<FormattedMessage {...messages.Paid} />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
||||||
{isExpired ? "Expired" : "Pay"}
|
{isExpired ? (
|
||||||
|
<FormattedMessage {...messages.Expired} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage {...messages.Pay} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export default function LoadMore({
|
export default function LoadMore({
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
shouldLoadMore,
|
shouldLoadMore,
|
||||||
@ -28,7 +31,7 @@ export default function LoadMore({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="mb10">
|
<div ref={ref} className="mb10">
|
||||||
{children ?? "Loading..."}
|
{children ?? <FormattedMessage {...messages.Loading} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { logout } from "State/Login";
|
import { logout } from "State/Login";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export default function LogoutButton() {
|
export default function LogoutButton() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -15,7 +18,7 @@ export default function LogoutButton() {
|
|||||||
navigate("/");
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
<FormattedMessage {...messages.Logout} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface MuteButtonProps {
|
interface MuteButtonProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
@ -9,11 +12,11 @@ const MuteButton = ({ pubkey }: MuteButtonProps) => {
|
|||||||
const { mute, unmute, isMuted } = useModeration();
|
const { mute, unmute, isMuted } = useModeration();
|
||||||
return isMuted(pubkey) ? (
|
return isMuted(pubkey) ? (
|
||||||
<button className="secondary" type="button" onClick={() => unmute(pubkey)}>
|
<button className="secondary" type="button" onClick={() => unmute(pubkey)}>
|
||||||
Unmute
|
<FormattedMessage {...messages.Unmute} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" onClick={() => mute(pubkey)}>
|
<button type="button" onClick={() => mute(pubkey)}>
|
||||||
Mute
|
<FormattedMessage {...messages.Mute} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import type { RootState } from "State/Store";
|
|
||||||
import MuteButton from "Element/MuteButton";
|
import MuteButton from "Element/MuteButton";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface MutedListProps {
|
export interface MutedListProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MutedList({ pubkey }: MutedListProps) {
|
export default function MutedList({ pubkey }: MutedListProps) {
|
||||||
const { muted, isMuted, mute, unmute, muteAll } = useModeration();
|
const { isMuted, muteAll } = useModeration();
|
||||||
const feed = useMutedFeed(pubkey);
|
const feed = useMutedFeed(pubkey);
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
return getMuted(feed.store, pubkey);
|
return getMuted(feed.store, pubkey);
|
||||||
@ -23,14 +24,19 @@ export default function MutedList({ pubkey }: MutedListProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<div className="flex mt10">
|
<div className="flex mt10">
|
||||||
<div className="f-grow bold">{`${pubkeys?.length} muted`}</div>
|
<div className="f-grow bold">
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.MuteCount}
|
||||||
|
values={{ n: pubkeys?.length }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
disabled={hasAllMuted || pubkeys.length === 0}
|
disabled={hasAllMuted || pubkeys.length === 0}
|
||||||
className="transparent"
|
className="transparent"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => muteAll(pubkeys)}
|
onClick={() => muteAll(pubkeys)}
|
||||||
>
|
>
|
||||||
Mute all
|
<FormattedMessage {...messages.MuteAll} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{pubkeys?.map((a) => {
|
{pubkeys?.map((a) => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
@ -18,6 +19,8 @@ import useEventPublisher from "Feed/EventPublisher";
|
|||||||
import { debounce, hexToBech32 } from "Util";
|
import { debounce, hexToBech32 } from "Util";
|
||||||
import { UserMetadata } from "Nostr";
|
import { UserMetadata } from "Nostr";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
type Nip05ServiceProps = {
|
type Nip05ServiceProps = {
|
||||||
name: string;
|
name: string;
|
||||||
service: URL | string;
|
service: URL | string;
|
||||||
@ -30,6 +33,7 @@ type ReduxStore = any;
|
|||||||
|
|
||||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const pubkey = useSelector<ReduxStore, string>((s) => s.login.publicKey);
|
const pubkey = useSelector<ReduxStore, string>((s) => s.login.publicKey);
|
||||||
const user = useUserProfile(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
@ -129,12 +133,12 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
|
|
||||||
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||||
let whyMap = new Map([
|
let whyMap = new Map([
|
||||||
["TOO_SHORT", "name too short"],
|
["TOO_SHORT", formatMessage(messages.TooShort)],
|
||||||
["TOO_LONG", "name too long"],
|
["TOO_LONG", formatMessage(messages.TooLong)],
|
||||||
["REGEX", "name has disallowed characters"],
|
["REGEX", formatMessage(messages.Regex)],
|
||||||
["REGISTERED", "name is registered"],
|
["REGISTERED", formatMessage(messages.Registered)],
|
||||||
["DISALLOWED_null", "name is blocked"],
|
["DISALLOWED_null", formatMessage(messages.Disallowed)],
|
||||||
["DISALLOWED_later", "name will be available later"],
|
["DISALLOWED_later", formatMessage(messages.DisalledLater)],
|
||||||
]);
|
]);
|
||||||
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
||||||
}
|
}
|
||||||
@ -171,10 +175,17 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
<h3>{props.name}</h3>
|
<h3>{props.name}</h3>
|
||||||
{props.about}
|
{props.about}
|
||||||
<p>
|
<p>
|
||||||
Find out more info about {props.name} at{" "}
|
<FormattedMessage
|
||||||
|
{...messages.FindMore}
|
||||||
|
values={{
|
||||||
|
service: props.name,
|
||||||
|
link: (
|
||||||
<a href={props.link} target="_blank" rel="noreferrer">
|
<a href={props.link} target="_blank" rel="noreferrer">
|
||||||
{props.link}
|
{props.link}
|
||||||
</a>
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
{error && <b className="error">{error.error}</b>}
|
{error && <b className="error">{error.error}</b>}
|
||||||
{!registerStatus && (
|
{!registerStatus && (
|
||||||
@ -196,7 +207,10 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
{availabilityResponse?.available && !registerStatus && (
|
{availabilityResponse?.available && !registerStatus && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="mr10">
|
<div className="mr10">
|
||||||
{availabilityResponse.quote?.price.toLocaleString()} sats
|
<FormattedMessage
|
||||||
|
{...messages.Sats}
|
||||||
|
values={{ n: availabilityResponse.quote?.price }}
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<small>{availabilityResponse.quote?.data.type}</small>
|
<small>{availabilityResponse.quote?.data.type}</small>
|
||||||
</div>
|
</div>
|
||||||
@ -208,14 +222,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<AsyncButton onClick={() => startBuy(handle, domain)}>
|
<AsyncButton onClick={() => startBuy(handle, domain)}>
|
||||||
Buy Now
|
<FormattedMessage {...messages.BuyNow} />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{availabilityResponse?.available === false && !registerStatus && (
|
{availabilityResponse?.available === false && !registerStatus && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<b className="error">
|
<b className="error">
|
||||||
Not available:{" "}
|
<FormattedMessage {...messages.NotAvailable} />{" "}
|
||||||
{mapError(
|
{mapError(
|
||||||
availabilityResponse.why!,
|
availabilityResponse.why!,
|
||||||
availabilityResponse.reasonTag || null
|
availabilityResponse.reasonTag || null
|
||||||
@ -227,32 +241,37 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
invoice={registerResponse?.invoice}
|
invoice={registerResponse?.invoice}
|
||||||
show={showInvoice}
|
show={showInvoice}
|
||||||
onClose={() => setShowInvoice(false)}
|
onClose={() => setShowInvoice(false)}
|
||||||
title={`Buying ${handle}@${domain}`}
|
title={formatMessage(messages.Buying, { item: `${handle}@${domain}` })}
|
||||||
/>
|
/>
|
||||||
{registerStatus?.paid && (
|
{registerStatus?.paid && (
|
||||||
<div className="flex f-col">
|
<div className="flex f-col">
|
||||||
<h4>Order Paid!</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.OrderPaid} />
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Your new NIP-05 handle is:{" "}
|
<FormattedMessage {...messages.NewNip} />{" "}
|
||||||
<code>
|
<code>
|
||||||
{handle}@{domain}
|
{handle}@{domain}
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
<h3>Account Support</h3>
|
<h3>
|
||||||
|
<FormattedMessage {...messages.AccountSupport} />
|
||||||
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Please make sure to save the following password in order to manage
|
<FormattedMessage {...messages.SavePassword} />
|
||||||
your handle in the future
|
|
||||||
</p>
|
</p>
|
||||||
<Copy text={registerStatus.password} />
|
<Copy text={registerStatus.password} />
|
||||||
<p>
|
<p>
|
||||||
Go to{" "}
|
<FormattedMessage {...messages.GoTo} />{" "}
|
||||||
<a href={props.supportLink} target="_blank" rel="noreferrer">
|
<a href={props.supportLink} target="_blank" rel="noreferrer">
|
||||||
account page
|
<FormattedMessage {...messages.AccountPage} />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<h4>Activate Now</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.ActivateNow} />
|
||||||
|
</h4>
|
||||||
<AsyncButton onClick={() => updateProfile(handle, domain)}>
|
<AsyncButton onClick={() => updateProfile(handle, domain)}>
|
||||||
Add to Profile
|
<FormattedMessage {...messages.AddToProfile} />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useNavigate, Link } from "react-router-dom";
|
import { useNavigate, Link } from "react-router-dom";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
@ -22,6 +23,8 @@ import { useUserProfiles } from "Feed/ProfileFeed";
|
|||||||
import { TaggedRawEvent, u256 } from "Nostr";
|
import { TaggedRawEvent, u256 } from "Nostr";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface NoteProps {
|
export interface NoteProps {
|
||||||
data?: TaggedRawEvent;
|
data?: TaggedRawEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -43,8 +46,12 @@ const HiddenNote = ({ children }: any) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="card note hidden-note">
|
<div className="card note hidden-note">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<p>This author has been muted</p>
|
<p>
|
||||||
<button onClick={() => setShow(true)}>Show</button>
|
<FormattedMessage {...messages.MutedAuthor} />
|
||||||
|
</p>
|
||||||
|
<button onClick={() => setShow(true)}>
|
||||||
|
<FormattedMessage {...messages.Show} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -76,6 +83,7 @@ export default function Note(props: NoteProps) {
|
|||||||
const baseClassname = `note card ${props.className ? props.className : ""}`;
|
const baseClassname = `note card ${props.className ? props.className : ""}`;
|
||||||
const [translated, setTranslated] = useState<Translation>();
|
const [translated, setTranslated] = useState<Translation>();
|
||||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
@ -87,7 +95,11 @@ export default function Note(props: NoteProps) {
|
|||||||
const transformBody = useCallback(() => {
|
const transformBody = useCallback(() => {
|
||||||
let body = ev?.Content ?? "";
|
let body = ev?.Content ?? "";
|
||||||
if (deletions?.length > 0) {
|
if (deletions?.length > 0) {
|
||||||
return <b className="error">Deleted</b>;
|
return (
|
||||||
|
<b className="error">
|
||||||
|
<FormattedMessage {...messages.Deleted} />
|
||||||
|
</b>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@ -157,7 +169,7 @@ export default function Note(props: NoteProps) {
|
|||||||
: mentions?.map(renderMention);
|
: mentions?.map(renderMention);
|
||||||
const others =
|
const others =
|
||||||
mentions.length > maxMentions
|
mentions.length > maxMentions
|
||||||
? ` & ${othersLength} other${othersLength > 1 ? "s" : ""}`
|
? formatMessage(messages.Others, { n: othersLength })
|
||||||
: "";
|
: "";
|
||||||
return (
|
return (
|
||||||
<div className="reply">
|
<div className="reply">
|
||||||
@ -181,7 +193,12 @@ export default function Note(props: NoteProps) {
|
|||||||
if (ev.Kind !== EventKind.TextNote) {
|
if (ev.Kind !== EventKind.TextNote) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Unknown event kind: {ev.Kind}</h4>
|
<h4>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.UnknownEventKind}
|
||||||
|
values={{ kind: ev.Kind }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
<pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre>
|
<pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -192,13 +209,20 @@ export default function Note(props: NoteProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="highlight">
|
<p className="highlight">
|
||||||
Translated from {translated.fromLanguage}:
|
<FormattedMessage
|
||||||
|
{...messages.TranslatedFrom}
|
||||||
|
values={{ lang: translated.fromLanguage }}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
{translated.text}
|
{translated.text}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else if (translated) {
|
} else if (translated) {
|
||||||
return <p className="highlight">Translation failed</p>;
|
return (
|
||||||
|
<p className="highlight">
|
||||||
|
<FormattedMessage {...messages.TranslationFailed} />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,19 +230,19 @@ export default function Note(props: NoteProps) {
|
|||||||
if (!inView) return null;
|
if (!inView) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{options.showHeader ? (
|
{options.showHeader && (
|
||||||
<div className="header flex">
|
<div className="header flex">
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
pubkey={ev.RootPubKey}
|
pubkey={ev.RootPubKey}
|
||||||
subHeader={replyTag() ?? undefined}
|
subHeader={replyTag() ?? undefined}
|
||||||
/>
|
/>
|
||||||
{options.showTime ? (
|
{options.showTime && (
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<NoteTime from={ev.CreatedAt * 1000} />
|
<NoteTime from={ev.CreatedAt * 1000} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||||
{transformBody()}
|
{transformBody()}
|
||||||
{translation()}
|
{translation()}
|
||||||
@ -228,7 +252,7 @@ export default function Note(props: NoteProps) {
|
|||||||
className="expand-note mt10 flex f-center"
|
className="expand-note mt10 flex f-center"
|
||||||
onClick={() => setShowMore(true)}
|
onClick={() => setShowMore(true)}
|
||||||
>
|
>
|
||||||
Show more
|
<FormattedMessage {...messages.ShowMore} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{options.showFooter && (
|
{options.showFooter && (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import "./NoteCreator.css";
|
import "./NoteCreator.css";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import Attachment from "Icons/Attachment";
|
import Attachment from "Icons/Attachment";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { openFile } from "Util";
|
import { openFile } from "Util";
|
||||||
@ -10,6 +11,8 @@ import ProfileImage from "Element/ProfileImage";
|
|||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import useFileUpload from "Upload";
|
import useFileUpload from "Upload";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface NotePreviewProps {
|
interface NotePreviewProps {
|
||||||
note: NEvent;
|
note: NEvent;
|
||||||
}
|
}
|
||||||
@ -121,10 +124,14 @@ export function NoteCreator(props: NoteCreatorProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="note-creator-actions">
|
<div className="note-creator-actions">
|
||||||
<button className="secondary" type="button" onClick={cancel}>
|
<button className="secondary" type="button" onClick={cancel}>
|
||||||
Cancel
|
<FormattedMessage {...messages.Cancel} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onSubmit}>
|
<button type="button" onClick={onSubmit}>
|
||||||
{replyTo ? "Reply" : "Send"}
|
{replyTo ? (
|
||||||
|
<FormattedMessage {...messages.Reply} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage {...messages.Send} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import {
|
import {
|
||||||
faTrash,
|
faTrash,
|
||||||
|
faHeart,
|
||||||
faRepeat,
|
faRepeat,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faCopy,
|
faCopy,
|
||||||
@ -19,10 +21,17 @@ import Zap from "Icons/Zap";
|
|||||||
import Reply from "Icons/Reply";
|
import Reply from "Icons/Reply";
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
|
import {
|
||||||
|
getReactions,
|
||||||
|
dedupeByPubkey,
|
||||||
|
hexToBech32,
|
||||||
|
normalizeReaction,
|
||||||
|
Reaction,
|
||||||
|
} from "Util";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
|
import Reactions from "Element/Reactions";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import { parseZap, ZapsSummary } from "Element/Zap";
|
import { parseZap, ParsedZap, ZapsSummary } from "Element/Zap";
|
||||||
import { useUserProfile } from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
@ -32,6 +41,8 @@ import { UserPreferences } from "State/Login";
|
|||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { TranslateHost } from "Const";
|
import { TranslateHost } from "Const";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface Translation {
|
export interface Translation {
|
||||||
text: string;
|
text: string;
|
||||||
fromLanguage: string;
|
fromLanguage: string;
|
||||||
@ -46,7 +57,7 @@ export interface NoteFooterProps {
|
|||||||
|
|
||||||
export default function NoteFooter(props: NoteFooterProps) {
|
export default function NoteFooter(props: NoteFooterProps) {
|
||||||
const { related, ev } = props;
|
const { related, ev } = props;
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const login = useSelector<RootState, HexKey | undefined>(
|
const login = useSelector<RootState, HexKey | undefined>(
|
||||||
(s) => s.login.publicKey
|
(s) => s.login.publicKey
|
||||||
);
|
);
|
||||||
@ -57,6 +68,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const author = useUserProfile(ev.RootPubKey);
|
const author = useUserProfile(ev.RootPubKey);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const [reply, setReply] = useState(false);
|
const [reply, setReply] = useState(false);
|
||||||
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
const [tip, setTip] = useState(false);
|
const [tip, setTip] = useState(false);
|
||||||
const isMine = ev.RootPubKey === login;
|
const isMine = ev.RootPubKey === login;
|
||||||
const lang = window.navigator.language;
|
const lang = window.navigator.language;
|
||||||
@ -68,31 +80,40 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
[related, ev]
|
[related, ev]
|
||||||
);
|
);
|
||||||
const reposts = useMemo(
|
const reposts = useMemo(
|
||||||
() => getReactions(related, ev.Id, EventKind.Repost),
|
() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)),
|
||||||
[related, ev]
|
[related, ev]
|
||||||
);
|
);
|
||||||
const zaps = useMemo(
|
const zaps = useMemo(() => {
|
||||||
() =>
|
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||||
getReactions(related, ev.Id, EventKind.ZapReceipt)
|
|
||||||
.map(parseZap)
|
.map(parseZap)
|
||||||
.filter((z) => z.valid && z.zapper !== ev.PubKey),
|
.filter((z) => z.valid && z.zapper !== ev.PubKey);
|
||||||
[related]
|
sortedZaps.sort((a, b) => b.amount - a.amount);
|
||||||
);
|
return sortedZaps;
|
||||||
|
}, [related]);
|
||||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
const didZap = zaps.some((a) => a.zapper === login);
|
const didZap = zaps.some((a) => a.zapper === login);
|
||||||
const groupReactions = useMemo(() => {
|
const groupReactions = useMemo(() => {
|
||||||
return reactions?.reduce(
|
const result = reactions?.reduce(
|
||||||
(acc, { content }) => {
|
(acc, reaction) => {
|
||||||
let r = normalizeReaction(content);
|
let kind = normalizeReaction(reaction.content);
|
||||||
const amount = acc[r] || 0;
|
const rs = acc[kind] || [];
|
||||||
return { ...acc, [r]: amount + 1 };
|
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return { ...acc, [kind]: [...rs, reaction] };
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[Reaction.Positive]: 0,
|
[Reaction.Positive]: [] as TaggedRawEvent[],
|
||||||
[Reaction.Negative]: 0,
|
[Reaction.Negative]: [] as TaggedRawEvent[],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
[Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
|
||||||
|
[Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
|
||||||
|
};
|
||||||
}, [reactions]);
|
}, [reactions]);
|
||||||
|
const positive = groupReactions[Reaction.Positive];
|
||||||
|
const negative = groupReactions[Reaction.Negative];
|
||||||
|
|
||||||
function hasReacted(emoji: string) {
|
function hasReacted(emoji: string) {
|
||||||
return reactions?.some(
|
return reactions?.some(
|
||||||
@ -115,7 +136,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
async function deleteEvent() {
|
async function deleteEvent() {
|
||||||
if (
|
if (
|
||||||
window.confirm(
|
window.confirm(
|
||||||
`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`
|
formatMessage(messages.ConfirmDeletion, { id: ev.Id.substring(0, 8) })
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
let evDelete = await publisher.delete(ev.Id);
|
let evDelete = await publisher.delete(ev.Id);
|
||||||
@ -127,7 +148,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
if (!hasReposted()) {
|
if (!hasReposted()) {
|
||||||
if (
|
if (
|
||||||
!prefs.confirmReposts ||
|
!prefs.confirmReposts ||
|
||||||
window.confirm(`Are you sure you want to repost: ${ev.Id}`)
|
window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.Id }))
|
||||||
) {
|
) {
|
||||||
let evRepost = await publisher.repost(ev);
|
let evRepost = await publisher.repost(ev);
|
||||||
publisher.broadcast(evRepost);
|
publisher.broadcast(evRepost);
|
||||||
@ -191,7 +212,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
<Heart />
|
<Heart />
|
||||||
</div>
|
</div>
|
||||||
<div className="reaction-pill-number">
|
<div className="reaction-pill-number">
|
||||||
{formatShort(groupReactions[Reaction.Positive])}
|
{formatShort(positive.length)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{repostIcon()}
|
{repostIcon()}
|
||||||
@ -250,42 +271,53 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{prefs.enableReactions && (
|
{prefs.enableReactions && (
|
||||||
<MenuItem onClick={() => react("-")}>
|
<MenuItem onClick={() => setShowReactions(true)}>
|
||||||
<Dislike />
|
<FontAwesomeIcon icon={faHeart} />
|
||||||
{formatShort(groupReactions[Reaction.Negative])}
|
<FormattedMessage {...messages.Reactions} />
|
||||||
Dislike
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => share()}>
|
<MenuItem onClick={() => share()}>
|
||||||
<FontAwesomeIcon icon={faShareNodes} />
|
<FontAwesomeIcon icon={faShareNodes} />
|
||||||
Share
|
<FormattedMessage {...messages.Share} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => copyId()}>
|
<MenuItem onClick={() => copyId()}>
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
Copy ID
|
<FormattedMessage {...messages.CopyID} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => mute(ev.PubKey)}>
|
<MenuItem onClick={() => mute(ev.PubKey)}>
|
||||||
<FontAwesomeIcon icon={faCommentSlash} />
|
<FontAwesomeIcon icon={faCommentSlash} />
|
||||||
Mute
|
<FormattedMessage {...messages.Mute} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{prefs.enableReactions && (
|
||||||
|
<MenuItem onClick={() => react("-")}>
|
||||||
|
<Dislike />
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.Dislike}
|
||||||
|
values={{ n: negative.length }}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem onClick={() => block(ev.PubKey)}>
|
<MenuItem onClick={() => block(ev.PubKey)}>
|
||||||
<FontAwesomeIcon icon={faBan} />
|
<FontAwesomeIcon icon={faBan} />
|
||||||
Block
|
<FormattedMessage {...messages.Block} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => translate()}>
|
<MenuItem onClick={() => translate()}>
|
||||||
<FontAwesomeIcon icon={faLanguage} />
|
<FontAwesomeIcon icon={faLanguage} />
|
||||||
Translate to {langNames.of(lang.split("-")[0])}
|
<FormattedMessage
|
||||||
|
{...messages.TranslateTo}
|
||||||
|
values={{ lang: langNames.of(lang.split("-")[0]) }}
|
||||||
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{prefs.showDebugMenus && (
|
{prefs.showDebugMenus && (
|
||||||
<MenuItem onClick={() => copyEvent()}>
|
<MenuItem onClick={() => copyEvent()}>
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
Copy Event JSON
|
<FormattedMessage {...messages.CopyJSON} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<MenuItem onClick={() => deleteEvent()}>
|
<MenuItem onClick={() => deleteEvent()}>
|
||||||
<FontAwesomeIcon icon={faTrash} className="red" />
|
<FontAwesomeIcon icon={faTrash} className="red" />
|
||||||
Delete
|
<FormattedMessage {...messages.Delete} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -326,6 +358,14 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
show={reply}
|
show={reply}
|
||||||
setShow={setReply}
|
setShow={setReply}
|
||||||
/>
|
/>
|
||||||
|
<Reactions
|
||||||
|
show={showReactions}
|
||||||
|
setShow={setShowReactions}
|
||||||
|
positive={positive}
|
||||||
|
negative={negative}
|
||||||
|
reposts={reposts}
|
||||||
|
zaps={zaps}
|
||||||
|
/>
|
||||||
<SendSats
|
<SendSats
|
||||||
svc={author?.lud16 || author?.lud06}
|
svc={author?.lud16 || author?.lud06}
|
||||||
onClose={() => setTip(false)}
|
onClose={() => setTip(false)}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedRelativeTime } from "react-intl";
|
||||||
|
|
||||||
const MinuteInMs = 1_000 * 60;
|
const MinuteInMs = 1_000 * 60;
|
||||||
const HourInMs = MinuteInMs * 60;
|
const HourInMs = MinuteInMs * 60;
|
||||||
@ -16,12 +17,12 @@ export default function NoteTime(props: NoteTimeProps) {
|
|||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "long",
|
timeStyle: "long",
|
||||||
}).format(from);
|
}).format(from);
|
||||||
const isoDate = new Date(from).toISOString();
|
const fromDate = new Date(from);
|
||||||
|
const isoDate = fromDate.toISOString();
|
||||||
|
const ago = new Date().getTime() - from;
|
||||||
|
const absAgo = Math.abs(ago);
|
||||||
|
|
||||||
function calcTime() {
|
function calcTime() {
|
||||||
let fromDate = new Date(from);
|
|
||||||
let ago = new Date().getTime() - from;
|
|
||||||
let absAgo = Math.abs(ago);
|
|
||||||
if (absAgo > DayInMs) {
|
if (absAgo > DayInMs) {
|
||||||
return fromDate.toLocaleDateString(undefined, {
|
return fromDate.toLocaleDateString(undefined, {
|
||||||
year: "2-digit",
|
year: "2-digit",
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import "./NoteToSelf.css";
|
import "./NoteToSelf.css";
|
||||||
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons";
|
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useUserProfile } from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import Nip05 from "Element/Nip05";
|
import Nip05 from "Element/Nip05";
|
||||||
import { profileLink } from "Util";
|
import { profileLink } from "Util";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface NoteToSelfProps {
|
export interface NoteToSelfProps {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
@ -18,7 +20,8 @@ function NoteLabel({ pubkey, link }: NoteToSelfProps) {
|
|||||||
const user = useUserProfile(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Note to Self <FontAwesomeIcon icon={faCertificate} size="xs" />
|
<FormattedMessage {...messages.NoteToSelf} />{" "}
|
||||||
|
<FontAwesomeIcon icon={faCertificate} size="xs" />
|
||||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
118
src/Element/Reactions.css
Normal file
118
src/Element/Reactions.css
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
.reactions-modal .modal-body {
|
||||||
|
padding: 0;
|
||||||
|
max-width: 586px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view {
|
||||||
|
padding: 24px 32px;
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
border-radius: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.reactions-view {
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-top: -160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .close {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 16px;
|
||||||
|
color: var(--font-secondary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .close:hover {
|
||||||
|
color: var(--font-tertiary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .reactions-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .reactions-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .body {
|
||||||
|
overflow: scroll;
|
||||||
|
height: 320px;
|
||||||
|
-ms-overflow-style: none; /* for Internet Explorer, Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-view .body::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-item .reaction-icon {
|
||||||
|
width: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-item .follow-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-item .zap-reaction-icon {
|
||||||
|
width: 52px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-item .zap-amount {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.reactions-view .tab.disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.reactions-item .reaction-icon {
|
||||||
|
width: 42px;
|
||||||
|
}
|
||||||
|
.reactions-item .avatar {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
.reactions-item .pfp .username {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.reactions-item .pfp .nip05 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.reactions-item button {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.reactions-item .zap-reaction-icon svg {
|
||||||
|
width: 12px;
|
||||||
|
height: l2px;
|
||||||
|
}
|
||||||
|
.reactions-item .zap-amount {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
166
src/Element/Reactions.tsx
Normal file
166
src/Element/Reactions.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import "./Reactions.css";
|
||||||
|
|
||||||
|
import { useState, useMemo, useEffect } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { TaggedRawEvent } from "Nostr";
|
||||||
|
|
||||||
|
import { formatShort } from "Number";
|
||||||
|
import Dislike from "Icons/Dislike";
|
||||||
|
import Heart from "Icons/Heart";
|
||||||
|
import ZapIcon from "Icons/Zap";
|
||||||
|
import { Tab } from "Element/Tabs";
|
||||||
|
import { ParsedZap } from "Element/Zap";
|
||||||
|
import ProfileImage from "Element/ProfileImage";
|
||||||
|
import FollowButton from "Element/FollowButton";
|
||||||
|
import Tabs from "Element/Tabs";
|
||||||
|
import Close from "Icons/Close";
|
||||||
|
import Modal from "Element/Modal";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
|
interface ReactionsProps {
|
||||||
|
show: boolean;
|
||||||
|
setShow(b: boolean): void;
|
||||||
|
positive: TaggedRawEvent[];
|
||||||
|
negative: TaggedRawEvent[];
|
||||||
|
reposts: TaggedRawEvent[];
|
||||||
|
zaps: ParsedZap[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Reactions = ({
|
||||||
|
show,
|
||||||
|
setShow,
|
||||||
|
positive,
|
||||||
|
negative,
|
||||||
|
reposts,
|
||||||
|
zaps,
|
||||||
|
}: ReactionsProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const onClose = () => setShow(false);
|
||||||
|
const likes = useMemo(() => {
|
||||||
|
const sorted = [...positive];
|
||||||
|
sorted.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
return sorted;
|
||||||
|
}, [positive]);
|
||||||
|
const dislikes = useMemo(() => {
|
||||||
|
const sorted = [...negative];
|
||||||
|
sorted.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
return sorted;
|
||||||
|
}, [negative]);
|
||||||
|
const total =
|
||||||
|
positive.length + negative.length + zaps.length + reposts.length;
|
||||||
|
const defaultTabs: Tab[] = [
|
||||||
|
{
|
||||||
|
text: formatMessage(messages.Likes, { n: likes.length }),
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: formatMessage(messages.Zaps, { n: zaps.length }),
|
||||||
|
value: 1,
|
||||||
|
disabled: zaps.length === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: formatMessage(messages.Reposts, { n: reposts.length }),
|
||||||
|
value: 2,
|
||||||
|
disabled: reposts.length === 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const tabs = defaultTabs.concat(
|
||||||
|
dislikes.length !== 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
text: formatMessage(messages.Dislikes, { n: dislikes.length }),
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tab, setTab] = useState(tabs[0]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!show) {
|
||||||
|
setTab(tabs[0]);
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return show ? (
|
||||||
|
<Modal className="reactions-modal" onClose={onClose}>
|
||||||
|
<div className="reactions-view">
|
||||||
|
<div className="close" onClick={onClose}>
|
||||||
|
<Close />
|
||||||
|
</div>
|
||||||
|
<div className="reactions-header">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.ReactionsCount}
|
||||||
|
values={{ n: total }}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
|
||||||
|
<div className="body" key={tab.value}>
|
||||||
|
{tab.value === 0 &&
|
||||||
|
likes.map((ev) => {
|
||||||
|
return (
|
||||||
|
<div key={ev.id} className="reactions-item">
|
||||||
|
<div className="reaction-icon">
|
||||||
|
{ev.content === "+" ? (
|
||||||
|
<Heart width={20} height={18} />
|
||||||
|
) : (
|
||||||
|
ev.content
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ProfileImage pubkey={ev.pubkey} />
|
||||||
|
<FollowButton pubkey={ev.pubkey} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{tab.value === 1 &&
|
||||||
|
zaps.map((z) => {
|
||||||
|
return (
|
||||||
|
<div key={z.id} className="reactions-item">
|
||||||
|
<div className="zap-reaction-icon">
|
||||||
|
<ZapIcon width={17} height={20} />
|
||||||
|
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||||
|
</div>
|
||||||
|
<ProfileImage
|
||||||
|
pubkey={z.zapper!}
|
||||||
|
subHeader={<>{z.content}</>}
|
||||||
|
/>
|
||||||
|
<FollowButton pubkey={z.zapper!} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{tab.value === 2 &&
|
||||||
|
reposts.map((ev) => {
|
||||||
|
return (
|
||||||
|
<div key={ev.id} className="reactions-item">
|
||||||
|
<div className="reaction-icon">
|
||||||
|
<Heart width={20} height={18} />
|
||||||
|
</div>
|
||||||
|
<ProfileImage pubkey={ev.pubkey} />
|
||||||
|
<FollowButton pubkey={ev.pubkey} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{tab.value === 3 &&
|
||||||
|
dislikes.map((ev) => {
|
||||||
|
return (
|
||||||
|
<div key={ev.id} className="reactions-item">
|
||||||
|
<div className="reaction-icon">
|
||||||
|
<Dislike width={20} height={18} />
|
||||||
|
</div>
|
||||||
|
<ProfileImage pubkey={ev.pubkey} />
|
||||||
|
<FollowButton pubkey={ev.pubkey} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Reactions;
|
@ -1,5 +1,6 @@
|
|||||||
import "./Relay.css";
|
import "./Relay.css";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
faPlug,
|
faPlug,
|
||||||
faSquareCheck,
|
faSquareCheck,
|
||||||
@ -15,7 +16,8 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import { setRelays } from "State/Login";
|
import { setRelays } from "State/Login";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { RelaySettings } from "Nostr/Connection";
|
import { RelaySettings } from "Nostr/Connection";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface RelayProps {
|
export interface RelayProps {
|
||||||
addr: string;
|
addr: string;
|
||||||
@ -23,6 +25,7 @@ export interface RelayProps {
|
|||||||
|
|
||||||
export default function Relay(props: RelayProps) {
|
export default function Relay(props: RelayProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const allRelaySettings = useSelector<
|
const allRelaySettings = useSelector<
|
||||||
RootState,
|
RootState,
|
||||||
@ -55,7 +58,7 @@ export default function Relay(props: RelayProps) {
|
|||||||
<div className="flex mb10">
|
<div className="flex mb10">
|
||||||
<b className="f-2">{name}</b>
|
<b className="f-2">{name}</b>
|
||||||
<div className="f-1">
|
<div className="f-1">
|
||||||
Write
|
<FormattedMessage {...messages.Write} />
|
||||||
<span
|
<span
|
||||||
className="checkmark"
|
className="checkmark"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -71,7 +74,7 @@ export default function Relay(props: RelayProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="f-1">
|
<div className="f-1">
|
||||||
Read
|
<FormattedMessage {...messages.Read} />
|
||||||
<span
|
<span
|
||||||
className="checkmark"
|
className="checkmark"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -91,8 +94,12 @@ export default function Relay(props: RelayProps) {
|
|||||||
<div className="f-grow">
|
<div className="f-grow">
|
||||||
<FontAwesomeIcon icon={faWifi} />{" "}
|
<FontAwesomeIcon icon={faWifi} />{" "}
|
||||||
{latency > 2000
|
{latency > 2000
|
||||||
? `${(latency / 1000).toFixed(0)} secs`
|
? formatMessage(messages.Seconds, {
|
||||||
: `${latency.toLocaleString()} ms`}
|
n: (latency / 1000).toFixed(0),
|
||||||
|
})
|
||||||
|
: formatMessage(messages.Milliseconds, {
|
||||||
|
n: latency.toLocaleString(),
|
||||||
|
})}
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,15 +61,6 @@
|
|||||||
line-height: 19px;
|
line-height: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lnurl-tip .btn {
|
|
||||||
background-color: inherit;
|
|
||||||
width: 210px;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lnurl-tip .btn:hover {
|
|
||||||
}
|
|
||||||
|
|
||||||
.amounts {
|
.amounts {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "./SendSats.css";
|
import "./SendSats.css";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import { bech32ToText } from "Util";
|
import { bech32ToText } from "Util";
|
||||||
@ -15,6 +16,8 @@ import Copy from "Element/Copy";
|
|||||||
import useWebln from "Hooks/useWebln";
|
import useWebln from "Hooks/useWebln";
|
||||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface LNURLService {
|
interface LNURLService {
|
||||||
nostrPubkey?: HexKey;
|
nostrPubkey?: HexKey;
|
||||||
minSendable?: number;
|
minSendable?: number;
|
||||||
@ -71,6 +74,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||||
const webln = useWebln(show);
|
const webln = useWebln(show);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const horizontalScroll = useHorizontalScroll();
|
const horizontalScroll = useHorizontalScroll();
|
||||||
|
|
||||||
@ -78,7 +82,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
if (show && !props.invoice) {
|
if (show && !props.invoice) {
|
||||||
loadService()
|
loadService()
|
||||||
.then((a) => setPayService(a!))
|
.then((a) => setPayService(a!))
|
||||||
.catch(() => setError("Failed to load LNURL service"));
|
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
||||||
} else {
|
} else {
|
||||||
setPayService(undefined);
|
setPayService(undefined);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@ -170,10 +174,10 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
payWebLNIfEnabled(data);
|
payWebLNIfEnabled(data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setError("Failed to load invoice");
|
setError(formatMessage(messages.InvoiceFail));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError("Failed to load invoice");
|
setError(formatMessage(messages.InvoiceFail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +191,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
className="f-grow mr10"
|
className="f-grow mr10"
|
||||||
placeholder="Custom"
|
placeholder={formatMessage(messages.Custom)}
|
||||||
value={customAmount}
|
value={customAmount}
|
||||||
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
@ -197,7 +201,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
disabled={!Boolean(customAmount)}
|
disabled={!Boolean(customAmount)}
|
||||||
onClick={() => selectAmount(customAmount!)}
|
onClick={() => selectAmount(customAmount!)}
|
||||||
>
|
>
|
||||||
Confirm
|
<FormattedMessage {...messages.Confirm} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -220,7 +224,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
if (invoice) return null;
|
if (invoice) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>Zap amount in sats</h3>
|
<h3>
|
||||||
|
<FormattedMessage {...messages.ZapAmount} />
|
||||||
|
</h3>
|
||||||
<div className="amounts" ref={horizontalScroll}>
|
<div className="amounts" ref={horizontalScroll}>
|
||||||
{serviceAmounts.map((a) => (
|
{serviceAmounts.map((a) => (
|
||||||
<span
|
<span
|
||||||
@ -235,15 +241,16 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
</div>
|
</div>
|
||||||
{payService && custom()}
|
{payService && custom()}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{(payService?.commentAllowed ?? 0) > 0 && (
|
{(payService?.commentAllowed ?? 0) > 0 ||
|
||||||
|
(payService?.nostrPubkey && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Comment"
|
placeholder={formatMessage(messages.Comment)}
|
||||||
className="f-grow"
|
className="f-grow"
|
||||||
maxLength={payService?.commentAllowed}
|
maxLength={payService?.commentAllowed || 120}
|
||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{(amount ?? 0) > 0 && (
|
{(amount ?? 0) > 0 && (
|
||||||
<button
|
<button
|
||||||
@ -252,9 +259,18 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
onClick={() => loadInvoice()}
|
onClick={() => loadInvoice()}
|
||||||
>
|
>
|
||||||
<div className="zap-action-container">
|
<div className="zap-action-container">
|
||||||
<Zap /> Zap
|
<Zap />
|
||||||
{target && ` ${target} `}
|
{target ? (
|
||||||
{formatShort(amount)} sats
|
<FormattedMessage
|
||||||
|
{...messages.ZapTarget}
|
||||||
|
values={{ target, n: formatShort(amount) }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.ZapSats}
|
||||||
|
values={{ n: formatShort(amount) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -281,7 +297,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.open(`lightning:${pr}`)}
|
onClick={() => window.open(`lightning:${pr}`)}
|
||||||
>
|
>
|
||||||
Open Wallet
|
<FormattedMessage {...messages.OpenWallet} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -297,7 +313,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
<div className="success-action">
|
<div className="success-action">
|
||||||
<p className="paid">
|
<p className="paid">
|
||||||
<Check className="success mr10" />
|
<Check className="success mr10" />
|
||||||
{success?.description ?? "Paid!"}
|
{success?.description ?? <FormattedMessage {...messages.Paid} />}
|
||||||
</p>
|
</p>
|
||||||
{success.url && (
|
{success.url && (
|
||||||
<p>
|
<p>
|
||||||
@ -310,8 +326,15 @@ export default function LNURLTip(props: LNURLTipProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultTitle = payService?.nostrPubkey ? "Send zap" : "Send sats";
|
const defaultTitle = payService?.nostrPubkey
|
||||||
const title = target ? `${defaultTitle} to ${target}` : defaultTitle;
|
? formatMessage(messages.SendZap)
|
||||||
|
: formatMessage(messages.SendSats);
|
||||||
|
const title = target
|
||||||
|
? formatMessage(messages.ToTarget, {
|
||||||
|
action: defaultTitle,
|
||||||
|
target,
|
||||||
|
})
|
||||||
|
: defaultTitle;
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
return (
|
return (
|
||||||
<Modal className="lnurl-modal" onClose={onClose}>
|
<Modal className="lnurl-modal" onClose={onClose}>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import "./ShowMore.css";
|
import "./ShowMore.css";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface ShowMoreProps {
|
interface ShowMoreProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
@ -6,16 +9,14 @@ interface ShowMoreProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShowMore = ({
|
const ShowMore = ({ text, onClick, className = "" }: ShowMoreProps) => {
|
||||||
text = "Show more",
|
const { formatMessage } = useIntl();
|
||||||
onClick,
|
const defaultText = formatMessage(messages.ShowMore);
|
||||||
className = "",
|
|
||||||
}: ShowMoreProps) => {
|
|
||||||
const classNames = className ? `show-more ${className}` : "show-more";
|
const classNames = className ? `show-more ${className}` : "show-more";
|
||||||
return (
|
return (
|
||||||
<div className="show-more-container">
|
<div className="show-more-container">
|
||||||
<button className={classNames} onClick={onClick}>
|
<button className={classNames} onClick={onClick}>
|
||||||
{text}
|
{text || defaultText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -34,3 +34,9 @@
|
|||||||
.tabs > div {
|
.tabs > div {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import "./Tabs.css";
|
import "./Tabs.css";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
text: string;
|
text: ReactElement | string;
|
||||||
value: number;
|
value: number;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabsProps {
|
interface TabsProps {
|
||||||
@ -18,8 +20,10 @@ interface TabElementProps extends Omit<TabsProps, "tabs"> {
|
|||||||
export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`tab ${tab.value === t.value ? "active" : ""}`}
|
className={`tab ${tab.value === t.value ? "active" : ""} ${
|
||||||
onClick={() => setTab(t)}
|
t.disabled ? "disabled" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => !t.disabled && setTab(t)}
|
||||||
>
|
>
|
||||||
{t.text}
|
{t.text}
|
||||||
</div>
|
</div>
|
||||||
@ -29,17 +33,9 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
|||||||
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
{tabs.map((t) => {
|
{tabs.map((t) => (
|
||||||
return (
|
<TabElement tab={tab} setTab={setTab} t={t} />
|
||||||
<div
|
))}
|
||||||
key={t.value}
|
|
||||||
className={`tab ${tab.value === t.value ? "active" : ""}`}
|
|
||||||
onClick={() => setTab(t)}
|
|
||||||
>
|
|
||||||
{t.text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
|
|||||||
import "./Textarea.css";
|
import "./Textarea.css";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||||
import emoji from "@jukben/emoji-search";
|
import emoji from "@jukben/emoji-search";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
@ -12,6 +13,8 @@ import { hexToBech32 } from "Util";
|
|||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "State/Users";
|
||||||
import { useQuery } from "State/Users/Hooks";
|
import { useQuery } from "State/Users/Hooks";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
interface EmojiItemProps {
|
interface EmojiItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
char: string;
|
char: string;
|
||||||
@ -43,6 +46,7 @@ const UserItem = (metadata: MetadataCache) => {
|
|||||||
|
|
||||||
const Textarea = ({ users, onChange, ...rest }: any) => {
|
const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const allUsers = useQuery(query);
|
const allUsers = useQuery(query);
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
|||||||
<ReactTextareaAutocomplete
|
<ReactTextareaAutocomplete
|
||||||
{...rest}
|
{...rest}
|
||||||
loadingComponent={() => <span>Loading....</span>}
|
loadingComponent={() => <span>Loading....</span>}
|
||||||
placeholder="What's on your mind?"
|
placeholder={formatMessage(messages.NotePlaceholder)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
textAreaComponent={TextareaAutosize}
|
textAreaComponent={TextareaAutosize}
|
||||||
trigger={{
|
trigger={{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "./Thread.css";
|
import "./Thread.css";
|
||||||
import { useMemo, useState, useEffect, ReactNode } from "react";
|
import { useMemo, useState, useEffect, ReactNode } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import { useNavigate, useLocation, Link } from "react-router-dom";
|
import { useNavigate, useLocation, Link } from "react-router-dom";
|
||||||
|
|
||||||
import { TaggedRawEvent, u256, HexKey } from "Nostr";
|
import { TaggedRawEvent, u256, HexKey } from "Nostr";
|
||||||
@ -11,7 +11,8 @@ import BackButton from "Element/BackButton";
|
|||||||
import Note from "Element/Note";
|
import Note from "Element/Note";
|
||||||
import NoteGhost from "Element/NoteGhost";
|
import NoteGhost from "Element/NoteGhost";
|
||||||
import Collapsed from "Element/Collapsed";
|
import Collapsed from "Element/Collapsed";
|
||||||
import type { RootState } from "State/Store";
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
function getParent(
|
function getParent(
|
||||||
ev: HexKey,
|
ev: HexKey,
|
||||||
@ -115,6 +116,7 @@ const ThreadNote = ({
|
|||||||
chains,
|
chains,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
}: ThreadNoteProps) => {
|
}: ThreadNoteProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const replies = getReplies(note.Id, chains);
|
const replies = getReplies(note.Id, chains);
|
||||||
const activeInReplies = replies.map((r) => r.Id).includes(active);
|
const activeInReplies = replies.map((r) => r.Id).includes(active);
|
||||||
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
||||||
@ -150,7 +152,7 @@ const ThreadNote = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Collapsed
|
<Collapsed
|
||||||
text="Show replies"
|
text={formatMessage(messages.ShowReplies)}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
>
|
>
|
||||||
@ -261,7 +263,7 @@ const TierThree = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onNavigate(from)}
|
onClick={() => onNavigate(from)}
|
||||||
>
|
>
|
||||||
Show replies
|
<FormattedMessage {...messages.ShowReplies} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faForward } from "@fortawesome/free-solid-svg-icons";
|
import { faForward } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
@ -14,6 +15,8 @@ import useModeration from "Hooks/useModeration";
|
|||||||
import ProfilePreview from "./ProfilePreview";
|
import ProfilePreview from "./ProfilePreview";
|
||||||
import Skeleton from "Element/Skeleton";
|
import Skeleton from "Element/Skeleton";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface TimelineProps {
|
export interface TimelineProps {
|
||||||
postsOnly: boolean;
|
postsOnly: boolean;
|
||||||
subject: TimelineSubject;
|
subject: TimelineSubject;
|
||||||
@ -96,8 +99,11 @@ export default function Timeline({
|
|||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
{latestFeed.length > 1 && (
|
{latestFeed.length > 1 && (
|
||||||
<div className="card latest-notes pointer" onClick={() => showLatest()}>
|
<div className="card latest-notes pointer" onClick={() => showLatest()}>
|
||||||
<FontAwesomeIcon icon={faForward} size="xl" />
|
<FontAwesomeIcon icon={faForward} size="xl" />{" "}
|
||||||
Show latest {latestFeed.length - 1} notes
|
<FormattedMessage
|
||||||
|
{...messages.ShowLatest}
|
||||||
|
values={{ n: latestFeed.length - 1 }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mainFeed.map(eventElement)}
|
{mainFeed.map(eventElement)}
|
||||||
|
@ -87,3 +87,7 @@
|
|||||||
.amount-number {
|
.amount-number {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.zap.note .body {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import "./Zap.css";
|
import "./Zap.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||||
@ -14,6 +15,8 @@ import Text from "Element/Text";
|
|||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
function findTag(e: TaggedRawEvent, tag: string) {
|
function findTag(e: TaggedRawEvent, tag: string) {
|
||||||
const maybeTag = e.tags.find((evTag) => {
|
const maybeTag = e.tags.find((evTag) => {
|
||||||
return evTag[0] === tag;
|
return evTag[0] === tag;
|
||||||
@ -55,7 +58,7 @@ function getZapper(zap: TaggedRawEvent, dhash: string): Zapper {
|
|||||||
return { isValid: false };
|
return { isValid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedZap {
|
export interface ParsedZap {
|
||||||
id: HexKey;
|
id: HexKey;
|
||||||
e?: HexKey;
|
e?: HexKey;
|
||||||
p: HexKey;
|
p: HexKey;
|
||||||
@ -91,23 +94,30 @@ const Zap = ({
|
|||||||
const { amount, content, zapper, valid, p } = zap;
|
const { amount, content, zapper, valid, p } = zap;
|
||||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||||
|
|
||||||
return valid ? (
|
return valid && zapper ? (
|
||||||
<div className="zap note card">
|
<div className="zap note card">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
{zapper ? <ProfileImage pubkey={zapper} /> : <div>Anon </div>}
|
<ProfileImage pubkey={zapper} />
|
||||||
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
|
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
|
||||||
<div className="amount">
|
<div className="amount">
|
||||||
<span className="amount-number">{formatShort(amount)}</span> sats
|
<span className="amount-number">
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.Sats}
|
||||||
|
values={{ n: formatShort(amount) }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{content.length > 0 && zapper && (
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<Text
|
<Text
|
||||||
creator={zapper || ""}
|
creator={zapper}
|
||||||
content={content}
|
content={content}
|
||||||
tags={[]}
|
tags={[]}
|
||||||
users={new Map()}
|
users={new Map()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
@ -118,8 +128,8 @@ interface ZapsSummaryProps {
|
|||||||
|
|
||||||
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||||
const sortedZaps = useMemo(() => {
|
const sortedZaps = useMemo(() => {
|
||||||
const pub = [...zaps.filter((z) => z.zapper)];
|
const pub = [...zaps.filter((z) => z.zapper && z.valid)];
|
||||||
const priv = [...zaps.filter((z) => !z.zapper)];
|
const priv = [...zaps.filter((z) => !z.zapper && z.valid)];
|
||||||
pub.sort((a, b) => b.amount - a.amount);
|
pub.sort((a, b) => b.amount - a.amount);
|
||||||
return pub.concat(priv);
|
return pub.concat(priv);
|
||||||
}, [zaps]);
|
}, [zaps]);
|
||||||
@ -129,8 +139,7 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [topZap, ...restZaps] = sortedZaps;
|
const [topZap, ...restZaps] = sortedZaps;
|
||||||
const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0);
|
const { zapper, amount } = topZap;
|
||||||
const { zapper, amount, content, valid } = topZap;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="zaps-summary">
|
<div className="zaps-summary">
|
||||||
@ -139,11 +148,15 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
|||||||
<div className="summary">
|
<div className="summary">
|
||||||
{zapper && <ProfileImage pubkey={zapper} />}
|
{zapper && <ProfileImage pubkey={zapper} />}
|
||||||
{restZaps.length > 0 && (
|
{restZaps.length > 0 && (
|
||||||
<span>
|
<FormattedMessage
|
||||||
and {restZaps.length} other{restZaps.length > 1 ? "s" : ""}
|
{...messages.Others}
|
||||||
</span>
|
values={{ n: restZaps.length }}
|
||||||
)}
|
/>
|
||||||
<span> zapped</span>
|
)}{" "}
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.OthersZapped}
|
||||||
|
values={{ n: restZaps.length }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
96
src/Element/messages.js
Normal file
96
src/Element/messages.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
import { addIdAndDefaultMessageToMessages } from "Util";
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
Cancel: "Cancel",
|
||||||
|
Reply: "Reply",
|
||||||
|
Send: "Send",
|
||||||
|
NotePlaceholder: "What's on your mind?",
|
||||||
|
Back: "Back",
|
||||||
|
Block: "Block",
|
||||||
|
Unblock: "Unblock",
|
||||||
|
MuteCount: "{n} muted",
|
||||||
|
Mute: "Mute",
|
||||||
|
MutedAuthor: "This author has been muted",
|
||||||
|
Others: ` & {n} {n, plural, =1 {other} other {others}}`,
|
||||||
|
Show: "Show",
|
||||||
|
Delete: "Delete",
|
||||||
|
Deleted: "Deleted",
|
||||||
|
Unmute: "Unmute",
|
||||||
|
MuteAll: "Mute all",
|
||||||
|
BlockCount: "{n} blocked",
|
||||||
|
JustNow: "Just now",
|
||||||
|
Follow: "Follow",
|
||||||
|
FollowAll: "Follow all",
|
||||||
|
Unfollow: "Unfollow",
|
||||||
|
FollowerCount: "{n} followers",
|
||||||
|
FollowingCount: "Follows {n}",
|
||||||
|
FollowsYou: "follows you",
|
||||||
|
Invoice: "Lightning Invoice",
|
||||||
|
PayInvoice: "Pay Invoice",
|
||||||
|
Expired: "Expired",
|
||||||
|
Pay: "Pay",
|
||||||
|
Paid: "Paid",
|
||||||
|
Loading: "Loading...",
|
||||||
|
Logout: "Logout",
|
||||||
|
ShowMore: "Show more",
|
||||||
|
TranslateTo: "Translate to {lang}",
|
||||||
|
TranslatedFrom: "Translated from {lang}",
|
||||||
|
TranslationFailed: "Translation failed",
|
||||||
|
UnknownEventKind: "Unknown event kind: {kind}",
|
||||||
|
ConfirmDeletion: `Are you sure you want to delete {id}`,
|
||||||
|
ConfirmRepost: `Are you sure you want to repost: {id}`,
|
||||||
|
Reactions: "Reactions",
|
||||||
|
ReactionsCount: "Reactions ({n})",
|
||||||
|
Share: "Share",
|
||||||
|
CopyID: "Copy ID",
|
||||||
|
CopyJSON: "Copy Event JSON",
|
||||||
|
Dislike: "{n} Dislike",
|
||||||
|
Sats: `{n} {n, plural, =1 {sat} other {sats}}`,
|
||||||
|
Zapped: "zapped",
|
||||||
|
OthersZapped: `{n, plural, =0 {} =1 {zapped} other {zapped}}`,
|
||||||
|
Likes: "Likes ({n})",
|
||||||
|
Zaps: "Zaps ({n})",
|
||||||
|
Dislikes: "Dislikes ({n})",
|
||||||
|
Reposts: "Reposts ({n})",
|
||||||
|
NoteToSelf: "Note to Self",
|
||||||
|
Read: "Read",
|
||||||
|
Write: "Write",
|
||||||
|
Seconds: "{n} secs",
|
||||||
|
Milliseconds: "{n} ms",
|
||||||
|
ShowLatest: "Show latest {n} notes",
|
||||||
|
LNURLFail: "Failed to load LNURL service",
|
||||||
|
InvoiceFail: "Failed to load invoice",
|
||||||
|
Custom: "Custom",
|
||||||
|
Confirm: "Confirm",
|
||||||
|
ZapAmount: "Zap amount in sats",
|
||||||
|
Comment: "Comment",
|
||||||
|
ZapTarget: "Zap {target} {n} sats",
|
||||||
|
ZapSats: "Zap {n} sats",
|
||||||
|
OpenWallet: "Open Wallet",
|
||||||
|
SendZap: "Send zap",
|
||||||
|
SendSats: "Send sats",
|
||||||
|
ToTarget: "{action} to {target}",
|
||||||
|
ShowReplies: "Show replies",
|
||||||
|
TooShort: "name too short",
|
||||||
|
TooLong: "name too long",
|
||||||
|
Regex: "name has disallowed characters",
|
||||||
|
Registered: "name is registered",
|
||||||
|
Disallowed: "name is blocked",
|
||||||
|
DisalledLater: "name will be available later",
|
||||||
|
BuyNow: "Buy Now",
|
||||||
|
NotAvailable: "Not available:",
|
||||||
|
Buying: "Buying {item}",
|
||||||
|
OrderPaid: "Order Paid!",
|
||||||
|
NewNip: "Your new NIP-05 handle is:",
|
||||||
|
ActivateNow: "Activate Now",
|
||||||
|
AddToProfile: "Add to Profile",
|
||||||
|
AccountPage: "account page",
|
||||||
|
AccountSupport: "Account Support",
|
||||||
|
GoTo: "Go to",
|
||||||
|
FindMore: "Find out more info about {service} at {link}",
|
||||||
|
SavePassword:
|
||||||
|
"Please make sure to save the following password in order to manage your handle in the future",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default addIdAndDefaultMessageToMessages(messages, "Element");
|
@ -118,6 +118,7 @@ export default function useTimelineFeed(
|
|||||||
sub.Id = `timeline-related:${subject.type}`;
|
sub.Id = `timeline-related:${subject.type}`;
|
||||||
sub.Kinds = new Set([
|
sub.Kinds = new Set([
|
||||||
EventKind.Reaction,
|
EventKind.Reaction,
|
||||||
|
EventKind.Repost,
|
||||||
EventKind.Deletion,
|
EventKind.Deletion,
|
||||||
EventKind.ZapReceipt,
|
EventKind.ZapReceipt,
|
||||||
]);
|
]);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
const Dislike = () => {
|
import IconProps from "./IconProps";
|
||||||
|
|
||||||
|
const Dislike = (props: IconProps) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="19"
|
width="19"
|
||||||
@ -6,6 +8,7 @@ const Dislike = () => {
|
|||||||
viewBox="0 0 19 20"
|
viewBox="0 0 19 20"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M13.1667 1.66667V10.8333M17.3333 8.16667V4.33334C17.3333 3.39992 17.3333 2.93321 17.1517 2.57669C16.9919 2.26308 16.7369 2.00812 16.4233 1.84833C16.0668 1.66667 15.6001 1.66667 14.6667 1.66667H5.76501C4.54711 1.66667 3.93816 1.66667 3.44632 1.88953C3.01284 2.08595 2.64442 2.40202 2.38437 2.8006C2.08931 3.25283 1.99672 3.8547 1.81153 5.05844L1.37563 7.89178C1.13137 9.47943 1.00925 10.2733 1.24484 10.8909C1.45162 11.4331 1.84054 11.8864 2.34494 12.1732C2.91961 12.5 3.72278 12.5 5.32912 12.5H6C6.46671 12.5 6.70007 12.5 6.87833 12.5908C7.03513 12.6707 7.16261 12.7982 7.24251 12.955C7.33334 13.1333 7.33334 13.3666 7.33334 13.8333V16.2785C7.33334 17.4133 8.25333 18.3333 9.3882 18.3333C9.65889 18.3333 9.90419 18.1739 10.0141 17.9266L12.8148 11.6252C12.9421 11.3385 13.0058 11.1952 13.1065 11.0902C13.1955 10.9973 13.3048 10.9263 13.4258 10.8827C13.5627 10.8333 13.7195 10.8333 14.0332 10.8333H14.6667C15.6001 10.8333 16.0668 10.8333 16.4233 10.6517C16.7369 10.4919 16.9919 10.2369 17.1517 9.92332C17.3333 9.5668 17.3333 9.10009 17.3333 8.16667Z"
|
d="M13.1667 1.66667V10.8333M17.3333 8.16667V4.33334C17.3333 3.39992 17.3333 2.93321 17.1517 2.57669C16.9919 2.26308 16.7369 2.00812 16.4233 1.84833C16.0668 1.66667 15.6001 1.66667 14.6667 1.66667H5.76501C4.54711 1.66667 3.93816 1.66667 3.44632 1.88953C3.01284 2.08595 2.64442 2.40202 2.38437 2.8006C2.08931 3.25283 1.99672 3.8547 1.81153 5.05844L1.37563 7.89178C1.13137 9.47943 1.00925 10.2733 1.24484 10.8909C1.45162 11.4331 1.84054 11.8864 2.34494 12.1732C2.91961 12.5 3.72278 12.5 5.32912 12.5H6C6.46671 12.5 6.70007 12.5 6.87833 12.5908C7.03513 12.6707 7.16261 12.7982 7.24251 12.955C7.33334 13.1333 7.33334 13.3666 7.33334 13.8333V16.2785C7.33334 17.4133 8.25333 18.3333 9.3882 18.3333C9.65889 18.3333 9.90419 18.1739 10.0141 17.9266L12.8148 11.6252C12.9421 11.3385 13.0058 11.1952 13.1065 11.0902C13.1955 10.9973 13.3048 10.9263 13.4258 10.8827C13.5627 10.8333 13.7195 10.8333 14.0332 10.8333H14.6667C15.6001 10.8333 16.0668 10.8333 16.4233 10.6517C16.7369 10.4919 16.9919 10.2369 17.1517 9.92332C17.3333 9.5668 17.3333 9.10009 17.3333 8.16667Z"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
const Heart = () => {
|
import IconProps from "./IconProps";
|
||||||
|
|
||||||
|
const Heart = (props: IconProps) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@ -6,6 +8,7 @@ const Heart = () => {
|
|||||||
viewBox="0 0 20 18"
|
viewBox="0 0 20 18"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import { HexKey, RawEvent } from "Nostr";
|
import { HexKey, RawEvent } from "Nostr";
|
||||||
@ -10,6 +11,8 @@ import { RootState } from "State/Store";
|
|||||||
import NoteToSelf from "Element/NoteToSelf";
|
import NoteToSelf from "Element/NoteToSelf";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
type DmChat = {
|
type DmChat = {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
unreadMessages: number;
|
unreadMessages: number;
|
||||||
@ -71,9 +74,11 @@ export default function MessagesPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h3 className="f-grow">Messages</h3>
|
<h3 className="f-grow">
|
||||||
|
<FormattedMessage {...messages.Messages} />
|
||||||
|
</h3>
|
||||||
<button type="button" onClick={() => markAllRead()}>
|
<button type="button" onClick={() => markAllRead()}>
|
||||||
Mark All Read
|
<FormattedMessage {...messages.MarkAllRead} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{chats
|
{chats
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import "./ProfilePage.css";
|
import "./ProfilePage.css";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -37,17 +37,18 @@ import Modal from "Element/Modal";
|
|||||||
import { ProxyImg } from "Element/ProxyImg";
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||||
|
|
||||||
const ProfileTab = {
|
import messages from "./messages";
|
||||||
Notes: { text: "Notes", value: 0 },
|
|
||||||
Reactions: { text: "Reactions", value: 1 },
|
const NOTES = 0;
|
||||||
Followers: { text: "Followers", value: 2 },
|
const REACTIONS = 1;
|
||||||
Follows: { text: "Follows", value: 3 },
|
const FOLLOWERS = 2;
|
||||||
Zaps: { text: "Zaps", value: 4 },
|
const FOLLOWS = 3;
|
||||||
Muted: { text: "Muted", value: 5 },
|
const ZAPS = 4;
|
||||||
Blocked: { text: "Blocked", value: 6 },
|
const MUTED = 5;
|
||||||
};
|
const BLOCKED = 6;
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const id = useMemo(() => parseId(params.id!), [params]);
|
const id = useMemo(() => parseId(params.id!), [params]);
|
||||||
@ -61,7 +62,6 @@ export default function ProfilePage() {
|
|||||||
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
|
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
|
||||||
const isMe = loginPubKey === id;
|
const isMe = loginPubKey === id;
|
||||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||||
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
|
||||||
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
||||||
const aboutText = user?.about || "";
|
const aboutText = user?.about || "";
|
||||||
const about = Text({
|
const about = Text({
|
||||||
@ -85,6 +85,16 @@ export default function ProfilePage() {
|
|||||||
}, [zapFeed.store, id]);
|
}, [zapFeed.store, id]);
|
||||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
const horizontalScroll = useHorizontalScroll();
|
const horizontalScroll = useHorizontalScroll();
|
||||||
|
const ProfileTab = {
|
||||||
|
Notes: { text: formatMessage(messages.Notes), value: NOTES },
|
||||||
|
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
|
||||||
|
Followers: { text: formatMessage(messages.Followers), value: FOLLOWERS },
|
||||||
|
Follows: { text: formatMessage(messages.Follows), value: FOLLOWS },
|
||||||
|
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS },
|
||||||
|
Muted: { text: formatMessage(messages.Muted), value: MUTED },
|
||||||
|
Blocked: { text: formatMessage(messages.Blocked), value: BLOCKED },
|
||||||
|
};
|
||||||
|
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTab(ProfileTab.Notes);
|
setTab(ProfileTab.Notes);
|
||||||
@ -149,8 +159,8 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tabContent() {
|
function tabContent() {
|
||||||
switch (tab) {
|
switch (tab.value) {
|
||||||
case ProfileTab.Notes:
|
case NOTES:
|
||||||
return (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
key={id}
|
key={id}
|
||||||
@ -164,10 +174,15 @@ export default function ProfilePage() {
|
|||||||
ignoreModeration={true}
|
ignoreModeration={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case ProfileTab.Zaps: {
|
case ZAPS: {
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h4 className="zaps-total">{formatShort(zapsTotal)} sats</h4>
|
<h4 className="zaps-total">
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.Sats}
|
||||||
|
values={{ n: formatShort(zapsTotal) }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
{zaps.map((z) => (
|
{zaps.map((z) => (
|
||||||
<ZapElement showZapped={false} zap={z} />
|
<ZapElement showZapped={false} zap={z} />
|
||||||
))}
|
))}
|
||||||
@ -175,11 +190,16 @@ export default function ProfilePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case ProfileTab.Follows: {
|
case FOLLOWS: {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h4>Following {follows.length}</h4>
|
<h4>
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.Following}
|
||||||
|
values={{ n: follows.length }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
{follows.map((a) => (
|
{follows.map((a) => (
|
||||||
<ProfilePreview
|
<ProfilePreview
|
||||||
key={a}
|
key={a}
|
||||||
@ -193,13 +213,13 @@ export default function ProfilePage() {
|
|||||||
return <FollowsList pubkey={id} />;
|
return <FollowsList pubkey={id} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ProfileTab.Followers: {
|
case FOLLOWERS: {
|
||||||
return <FollowersList pubkey={id} />;
|
return <FollowersList pubkey={id} />;
|
||||||
}
|
}
|
||||||
case ProfileTab.Muted: {
|
case MUTED: {
|
||||||
return isMe ? <BlockList variant="muted" /> : <MutedList pubkey={id} />;
|
return isMe ? <BlockList variant="muted" /> : <MutedList pubkey={id} />;
|
||||||
}
|
}
|
||||||
case ProfileTab.Blocked: {
|
case BLOCKED: {
|
||||||
return isMe ? <BlockList variant="blocked" /> : null;
|
return isMe ? <BlockList variant="blocked" /> : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,7 +253,7 @@ export default function ProfilePage() {
|
|||||||
<>
|
<>
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
<button type="button" onClick={() => navigate("/settings")}>
|
<button type="button" onClick={() => navigate("/settings")}>
|
||||||
Settings
|
<FormattedMessage {...messages.Settings} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -2,6 +2,7 @@ import "./Root.css";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import Tabs, { Tab } from "Element/Tabs";
|
import Tabs, { Tab } from "Element/Tabs";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
@ -9,10 +10,21 @@ import Timeline from "Element/Timeline";
|
|||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { TimelineSubject } from "Feed/TimelineFeed";
|
import { TimelineSubject } from "Feed/TimelineFeed";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const RootTab: Record<string, Tab> = {
|
const RootTab: Record<string, Tab> = {
|
||||||
Posts: { text: "Posts", value: 0 },
|
Posts: {
|
||||||
PostsAndReplies: { text: "Conversations", value: 1 },
|
text: <FormattedMessage {...messages.Posts} />,
|
||||||
Global: { text: "Global", value: 2 },
|
value: 0,
|
||||||
|
},
|
||||||
|
PostsAndReplies: {
|
||||||
|
text: <FormattedMessage {...messages.Conversations} />,
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
Global: {
|
||||||
|
text: <FormattedMessage {...messages.Global} />,
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootPage() {
|
export default function RootPage() {
|
||||||
@ -25,10 +37,16 @@ export default function RootPage() {
|
|||||||
function followHints() {
|
function followHints() {
|
||||||
if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
|
if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
|
||||||
return (
|
return (
|
||||||
<>
|
<FormattedMessage
|
||||||
Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to
|
{...messages.NoFollows}
|
||||||
follow some recommended nostrich's!
|
values={{
|
||||||
</>
|
newUsersPage: (
|
||||||
|
<Link to={"/new"}>
|
||||||
|
<FormattedMessage {...messages.NewUsers} />
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useIntl, FormattedMessage } from "react-intl";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Timeline from "Element/Timeline";
|
import Timeline from "Element/Timeline";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@ -6,8 +7,11 @@ import { router } from "index";
|
|||||||
import { SearchRelays } from "Const";
|
import { SearchRelays } from "Const";
|
||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const SearchPage = () => {
|
const SearchPage = () => {
|
||||||
const params: any = useParams();
|
const params: any = useParams();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [search, setSearch] = useState<string>();
|
const [search, setSearch] = useState<string>();
|
||||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||||
|
|
||||||
@ -39,12 +43,14 @@ const SearchPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h2>Search</h2>
|
<h2>
|
||||||
|
<FormattedMessage {...messages.Search} />
|
||||||
|
</h2>
|
||||||
<div className="flex mb10">
|
<div className="flex mb10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="f-grow mr10"
|
className="f-grow mr10"
|
||||||
placeholder="Search.."
|
placeholder={formatMessage(messages.SearchPlaceholder)}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
|
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
|
||||||
import SettingsIndex from "Pages/settings/Index";
|
import SettingsIndex from "Pages/settings/Index";
|
||||||
import Profile from "Pages/settings/Profile";
|
import Profile from "Pages/settings/Profile";
|
||||||
@ -5,13 +6,15 @@ import Relay from "Pages/settings/Relays";
|
|||||||
import Preferences from "Pages/settings/Preferences";
|
import Preferences from "Pages/settings/Preferences";
|
||||||
import RelayInfo from "Pages/settings/RelayInfo";
|
import RelayInfo from "Pages/settings/RelayInfo";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<h2 onClick={() => navigate("/settings")} className="pointer">
|
<h2 onClick={() => navigate("/settings")} className="pointer">
|
||||||
Settings
|
<FormattedMessage {...messages.Settings} />
|
||||||
</h2>
|
</h2>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { ApiHost } from "Const";
|
import { ApiHost } from "Const";
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
import "./Verification.css";
|
import "./Verification.css";
|
||||||
|
|
||||||
export default function VerificationPage() {
|
export default function VerificationPage() {
|
||||||
@ -10,42 +14,37 @@ export default function VerificationPage() {
|
|||||||
service: `${ApiHost}/api/v1/n5sp`,
|
service: `${ApiHost}/api/v1/n5sp`,
|
||||||
link: "https://snort.social/",
|
link: "https://snort.social/",
|
||||||
supportLink: "https://snort.social/help",
|
supportLink: "https://snort.social/help",
|
||||||
about: (
|
about: <FormattedMessage {...messages.SnortSocialNip} />,
|
||||||
<>
|
|
||||||
Our very own NIP-05 verification service, help support the development
|
|
||||||
of this site and get a shiny special badge on our site!
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nostr Plebs",
|
name: "Nostr Plebs",
|
||||||
service: "https://nostrplebs.com/api/v1",
|
service: "https://nostrplebs.com/api/v1",
|
||||||
link: "https://nostrplebs.com/",
|
link: "https://nostrplebs.com/",
|
||||||
supportLink: "https://nostrplebs.com/manage",
|
supportLink: "https://nostrplebs.com/manage",
|
||||||
about: (
|
about: <FormattedMessage {...messages.NostrPlebsNip} />,
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Nostr Plebs is one of the first NIP-05 providers in the space and
|
|
||||||
offers a good collection of domains at reasonable prices
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-content verification">
|
<div className="main-content verification">
|
||||||
<h2>Get Verified</h2>
|
<h2>
|
||||||
|
<FormattedMessage {...messages.GetVerified} />
|
||||||
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
NIP-05 is a DNS based verification spec which helps to validate you as a
|
<FormattedMessage {...messages.Nip05} />
|
||||||
real user.
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.Nip05Pros} />
|
||||||
</p>
|
</p>
|
||||||
<p>Getting NIP-05 verified can help:</p>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Prevent fake accounts from imitating you</li>
|
|
||||||
<li>Make your profile easier to find and share</li>
|
|
||||||
<li>
|
<li>
|
||||||
Fund developers and platforms providing NIP-05 verification services
|
<FormattedMessage {...messages.AvoidImpersonators} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.EasierToFind} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<FormattedMessage {...messages.Funding} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -3,6 +3,35 @@ import { addIdAndDefaultMessageToMessages } from "Util";
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
Login: "Login",
|
Login: "Login",
|
||||||
|
Posts: "Posts",
|
||||||
|
Conversations: "Conversations",
|
||||||
|
Global: "Global",
|
||||||
|
NewUsers: "New users page",
|
||||||
|
NoFollows:
|
||||||
|
"Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
|
||||||
|
Notes: "Notes",
|
||||||
|
Reactions: "Reactions",
|
||||||
|
Followers: "Followers",
|
||||||
|
Follows: "Follows",
|
||||||
|
Zaps: "Zaps",
|
||||||
|
Muted: "Muted",
|
||||||
|
Blocked: "Blocked",
|
||||||
|
Sats: "{n} {n, plural, =1 {sat} other {sats}}",
|
||||||
|
Following: "Following {n}",
|
||||||
|
Settings: "Settings",
|
||||||
|
Search: "Search",
|
||||||
|
SearchPlaceholder: "Search...",
|
||||||
|
Messages: "Messages",
|
||||||
|
MarkAllRead: "Mark All Read",
|
||||||
|
GetVerified: "Get Verified",
|
||||||
|
Nip05: `NIP-05 is a DNS based verification spec which helps to validate you as a real user.`,
|
||||||
|
Nip05Pros: `Getting NIP-05 verified can help:`,
|
||||||
|
AvoidImpersonators: "Prevent fake accounts from imitating you",
|
||||||
|
EasierToFind: "Make your profile easier to find and share",
|
||||||
|
Funding:
|
||||||
|
"Fund developers and platforms providing NIP-05 verification services",
|
||||||
|
SnortSocialNip: `Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!`,
|
||||||
|
NostrPlebsNip: `Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default addIdAndDefaultMessageToMessages(messages, 'Pages');
|
export default addIdAndDefaultMessageToMessages(messages, "Pages");
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import "./Index.css";
|
import "./Index.css";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import ArrowFront from "Icons/ArrowFront";
|
import ArrowFront from "Icons/ArrowFront";
|
||||||
@ -8,9 +8,10 @@ import Profile from "Icons/Profile";
|
|||||||
import Relay from "Icons/Relay";
|
import Relay from "Icons/Relay";
|
||||||
import Heart from "Icons/Heart";
|
import Heart from "Icons/Heart";
|
||||||
import Logout from "Icons/Logout";
|
import Logout from "Icons/Logout";
|
||||||
|
|
||||||
import { logout } from "State/Login";
|
import { logout } from "State/Login";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const SettingsIndex = () => {
|
const SettingsIndex = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -27,7 +28,9 @@ const SettingsIndex = () => {
|
|||||||
<div className="mr10">
|
<div className="mr10">
|
||||||
<Profile />
|
<Profile />
|
||||||
</div>
|
</div>
|
||||||
<span>Profile</span>
|
<span>
|
||||||
|
<FormattedMessage {...messages.Profile} />
|
||||||
|
</span>
|
||||||
<div className="align-end">
|
<div className="align-end">
|
||||||
<ArrowFront />
|
<ArrowFront />
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +39,7 @@ const SettingsIndex = () => {
|
|||||||
<div className="mr10">
|
<div className="mr10">
|
||||||
<Relay />
|
<Relay />
|
||||||
</div>
|
</div>
|
||||||
Relays
|
<FormattedMessage {...messages.Relays} />
|
||||||
<div className="align-end">
|
<div className="align-end">
|
||||||
<ArrowFront />
|
<ArrowFront />
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +48,7 @@ const SettingsIndex = () => {
|
|||||||
<div className="mr10">
|
<div className="mr10">
|
||||||
<Gear />
|
<Gear />
|
||||||
</div>
|
</div>
|
||||||
Preferences
|
<FormattedMessage {...messages.Preferences} />
|
||||||
<div className="align-end">
|
<div className="align-end">
|
||||||
<ArrowFront />
|
<ArrowFront />
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +66,7 @@ const SettingsIndex = () => {
|
|||||||
<div className="mr10">
|
<div className="mr10">
|
||||||
<Logout />
|
<Logout />
|
||||||
</div>
|
</div>
|
||||||
Log Out
|
<FormattedMessage {...messages.LogOut} />
|
||||||
<div className="align-end">
|
<div className="align-end">
|
||||||
<ArrowFront />
|
<ArrowFront />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import "./Preferences.css";
|
||||||
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
|
import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import "./Preferences.css";
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const PreferencesPage = () => {
|
const PreferencesPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -11,11 +16,15 @@ const PreferencesPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="preferences">
|
<div className="preferences">
|
||||||
<h3>Preferences</h3>
|
<h3>
|
||||||
|
<FormattedMessage {...messages.Preferences} />
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Theme</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.Theme} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
@ -29,18 +38,25 @@ const PreferencesPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="system">System (Default)</option>
|
<option value="system">
|
||||||
<option value="light">Light</option>
|
<FormattedMessage {...messages.System} />
|
||||||
<option value="dark">Dark</option>
|
</option>
|
||||||
|
<option value="light">
|
||||||
|
<FormattedMessage {...messages.Light} />
|
||||||
|
</option>
|
||||||
|
<option value="dark">
|
||||||
|
<FormattedMessage {...messages.Dark} />
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Automatically load media</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.AutoloadMedia} />
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Media in posts will automatically be shown for selected people,
|
<FormattedMessage {...messages.AutoloadMediaHelp} />
|
||||||
otherwise only the link will show
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -55,17 +71,27 @@ const PreferencesPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="none">None</option>
|
<option value="none">
|
||||||
<option value="follows-only">Follows only</option>
|
<FormattedMessage {...messages.None} />
|
||||||
<option value="all">All</option>
|
</option>
|
||||||
|
<option value="follows-only">
|
||||||
|
<FormattedMessage {...messages.FollowsOnly} />
|
||||||
|
</option>
|
||||||
|
<option value="all">
|
||||||
|
<FormattedMessage {...messages.All} />
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card flex f-col">
|
<div className="card flex f-col">
|
||||||
<div className="flex w-max">
|
<div className="flex w-max">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Image proxy service</div>
|
<div>
|
||||||
<small>Use imgproxy to compress images</small>
|
<FormattedMessage {...messages.ImgProxy} />
|
||||||
|
</div>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage {...messages.ImgProxyHelp} />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@ -85,12 +111,14 @@ const PreferencesPage = () => {
|
|||||||
{perf.imgProxyConfig && (
|
{perf.imgProxyConfig && (
|
||||||
<div className="w-max mt10 form">
|
<div className="w-max mt10 form">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Service Url</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.ServiceUrl} />
|
||||||
|
</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={perf.imgProxyConfig?.url}
|
value={perf.imgProxyConfig?.url}
|
||||||
placeholder="Url.."
|
placeholder="URL.."
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
setPreferences({
|
setPreferences({
|
||||||
@ -106,7 +134,9 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Service Key</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.ServiceKey} />
|
||||||
|
</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -127,7 +157,9 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Service Salt</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.ServiceSalt} />
|
||||||
|
</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -152,10 +184,11 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Enable reactions</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.EnableReactions} />
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Reactions will be shown on every page, if disabled no reactions will
|
<FormattedMessage {...messages.EnableReactionsHelp} />
|
||||||
be shown
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -172,8 +205,12 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Confirm reposts</div>
|
<div>
|
||||||
<small>Reposts need to be manually confirmed</small>
|
<FormattedMessage {...messages.ConfirmReposts} />
|
||||||
|
</div>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage {...messages.ConfirmRepostsHelp} />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@ -189,9 +226,11 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Automatically show latest notes</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.ShowLatest} />
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Notes will stream in real time into global and posts tab
|
<FormattedMessage {...messages.ShowLatestHelp} />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -208,9 +247,11 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>File upload service</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.FileUpload} />
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Pick which upload service you want to upload attachments to
|
<FormattedMessage {...messages.FileUploadHelp} />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -225,7 +266,9 @@ const PreferencesPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="void.cat">void.cat (Default)</option>
|
<option value="void.cat">
|
||||||
|
void.cat <FormattedMessage {...messages.Default} />
|
||||||
|
</option>
|
||||||
<option value="nostr.build">nostr.build</option>
|
<option value="nostr.build">nostr.build</option>
|
||||||
<option value="nostrimg.com">nostrimg.com</option>
|
<option value="nostrimg.com">nostrimg.com</option>
|
||||||
</select>
|
</select>
|
||||||
@ -233,10 +276,11 @@ const PreferencesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="card flex">
|
<div className="card flex">
|
||||||
<div className="flex f-col f-grow">
|
<div className="flex f-col f-grow">
|
||||||
<div>Debug Menus</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.DebugMenus} />
|
||||||
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Shows "Copy ID" and "Copy Event JSON" in the context menu on each
|
<FormattedMessage {...messages.DebugMenusHelp} />
|
||||||
message
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "./Profile.css";
|
import "./Profile.css";
|
||||||
import Nostrich from "nostrich.webp";
|
import Nostrich from "nostrich.webp";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -15,6 +15,8 @@ import { RootState } from "State/Store";
|
|||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import useFileUpload from "Upload";
|
import useFileUpload from "Upload";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export interface ProfileSettingsProps {
|
export interface ProfileSettingsProps {
|
||||||
avatar?: boolean;
|
avatar?: boolean;
|
||||||
banner?: boolean;
|
banner?: boolean;
|
||||||
@ -112,7 +114,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="editor form">
|
<div className="editor form">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Name:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.Name} />:
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -122,7 +126,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Display name:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.DisplayName} />:
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -132,7 +138,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group form-col">
|
<div className="form-group form-col">
|
||||||
<div>About:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.About} />:
|
||||||
|
</div>
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<textarea
|
<textarea
|
||||||
className="w-max"
|
className="w-max"
|
||||||
@ -142,7 +150,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>Website:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.Website} />:
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -152,7 +162,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>NIP-05:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.Nip05} />:
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -162,12 +174,14 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => navigate("/verification")}>
|
<button type="button" onClick={() => navigate("/verification")}>
|
||||||
<FontAwesomeIcon icon={faShop} />
|
<FontAwesomeIcon icon={faShop} />
|
||||||
Buy
|
<FormattedMessage {...messages.Buy} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div>LN Address:</div>
|
<div>
|
||||||
|
<FormattedMessage {...messages.LnAddress} />:
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -180,7 +194,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
<div></div>
|
<div></div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" onClick={() => saveProfile()}>
|
<button type="button" onClick={() => saveProfile()}>
|
||||||
Save
|
<FormattedMessage {...messages.Save} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -195,20 +209,24 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
<div className="flex f-center image-settings">
|
<div className="flex f-center image-settings">
|
||||||
{(props.avatar ?? true) && (
|
{(props.avatar ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<h2>Avatar</h2>
|
<h2>
|
||||||
|
<FormattedMessage {...messages.Avatar} />
|
||||||
|
</h2>
|
||||||
<div
|
<div
|
||||||
style={{ backgroundImage: `url(${avatarPicture})` }}
|
style={{ backgroundImage: `url(${avatarPicture})` }}
|
||||||
className="avatar"
|
className="avatar"
|
||||||
>
|
>
|
||||||
<div className="edit" onClick={() => setNewAvatar()}>
|
<div className="edit" onClick={() => setNewAvatar()}>
|
||||||
Edit
|
<FormattedMessage {...messages.Edit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(props.banner ?? true) && (
|
{(props.banner ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<h2>Header</h2>
|
<h2>
|
||||||
|
<FormattedMessage {...messages.Banner} />
|
||||||
|
</h2>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${
|
backgroundImage: `url(${
|
||||||
@ -218,7 +236,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
className="banner"
|
className="banner"
|
||||||
>
|
>
|
||||||
<div className="edit" onClick={() => setNewBanner()}>
|
<div className="edit" onClick={() => setNewBanner()}>
|
||||||
Edit
|
<FormattedMessage {...messages.Edit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,12 +249,16 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
<h3>Edit Profile</h3>
|
<h3>
|
||||||
|
<FormattedMessage {...messages.EditProfile} />
|
||||||
|
</h3>
|
||||||
{settings()}
|
{settings()}
|
||||||
{privKey && (props.privateKey ?? true) && (
|
{privKey && (props.privateKey ?? true) && (
|
||||||
<div className="flex f-col bg-grey">
|
<div className="flex f-col bg-grey">
|
||||||
<div>
|
<div>
|
||||||
<h4>Your Private Key Is (do not share this with anyone):</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.PrivateKey} />:
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Copy text={hexToBech32("nsec", privKey)} />
|
<Copy text={hexToBech32("nsec", privKey)} />
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import ProfilePreview from "Element/ProfilePreview";
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
import useRelayState from "Feed/RelayState";
|
import useRelayState from "Feed/RelayState";
|
||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
@ -6,6 +7,8 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||||||
import { removeRelay } from "State/Login";
|
import { removeRelay } from "State/Login";
|
||||||
import { parseId } from "Util";
|
import { parseId } from "Util";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const RelayInfo = () => {
|
const RelayInfo = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -20,7 +23,7 @@ const RelayInfo = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>
|
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>
|
||||||
Relays
|
<FormattedMessage {...messages.Relays} />
|
||||||
</h3>
|
</h3>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3>{stats?.info?.name}</h3>
|
<h3>{stats?.info?.name}</h3>
|
||||||
@ -28,13 +31,17 @@ const RelayInfo = () => {
|
|||||||
|
|
||||||
{stats?.info?.pubkey && (
|
{stats?.info?.pubkey && (
|
||||||
<>
|
<>
|
||||||
<h4>Owner</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.Owner} />
|
||||||
|
</h4>
|
||||||
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
|
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{stats?.info?.software && (
|
{stats?.info?.software && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h4 className="f-grow">Software</h4>
|
<h4 className="f-grow">
|
||||||
|
<FormattedMessage {...messages.Software} />
|
||||||
|
</h4>
|
||||||
<div className="flex f-col">
|
<div className="flex f-col">
|
||||||
{stats.info.software.startsWith("http") ? (
|
{stats.info.software.startsWith("http") ? (
|
||||||
<a href={stats.info.software} target="_blank" rel="noreferrer">
|
<a href={stats.info.software} target="_blank" rel="noreferrer">
|
||||||
@ -52,7 +59,9 @@ const RelayInfo = () => {
|
|||||||
)}
|
)}
|
||||||
{stats?.info?.contact && (
|
{stats?.info?.contact && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h4 className="f-grow">Contact</h4>
|
<h4 className="f-grow">
|
||||||
|
<FormattedMessage {...messages.Contact} />
|
||||||
|
</h4>
|
||||||
<a
|
<a
|
||||||
href={`${
|
href={`${
|
||||||
stats.info.contact.startsWith("mailto:") ? "" : "mailto:"
|
stats.info.contact.startsWith("mailto:") ? "" : "mailto:"
|
||||||
@ -66,7 +75,9 @@ const RelayInfo = () => {
|
|||||||
)}
|
)}
|
||||||
{stats?.info?.supported_nips && (
|
{stats?.info?.supported_nips && (
|
||||||
<>
|
<>
|
||||||
<h4>Supports</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.Supports} />
|
||||||
|
</h4>
|
||||||
<div className="f-grow">
|
<div className="f-grow">
|
||||||
{stats.info.supported_nips.map((a) => (
|
{stats.info.supported_nips.map((a) => (
|
||||||
<span
|
<span
|
||||||
@ -93,7 +104,7 @@ const RelayInfo = () => {
|
|||||||
navigate("/settings/relays");
|
navigate("/settings/relays");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
<FormattedMessage {...messages.Remove} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import Relay from "Element/Relay";
|
import Relay from "Element/Relay";
|
||||||
@ -7,6 +8,8 @@ import { RootState } from "State/Store";
|
|||||||
import { RelaySettings } from "Nostr/Connection";
|
import { RelaySettings } from "Nostr/Connection";
|
||||||
import { setRelays } from "State/Login";
|
import { setRelays } from "State/Login";
|
||||||
|
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
const RelaySettingsPage = () => {
|
const RelaySettingsPage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
@ -24,7 +27,9 @@ const RelaySettingsPage = () => {
|
|||||||
function addRelay() {
|
function addRelay() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Add Relays</h4>
|
<h4>
|
||||||
|
<FormattedMessage {...messages.AddRelays} />
|
||||||
|
</h4>
|
||||||
<div className="flex mb10">
|
<div className="flex mb10">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -35,7 +40,7 @@ const RelaySettingsPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="secondary mb10" onClick={() => addNewRelay()}>
|
<button className="secondary mb10" onClick={() => addNewRelay()}>
|
||||||
Add
|
<FormattedMessage {...messages.Add} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -66,7 +71,7 @@ const RelaySettingsPage = () => {
|
|||||||
<div className="flex mt10">
|
<div className="flex mt10">
|
||||||
<div className="f-grow"></div>
|
<div className="f-grow"></div>
|
||||||
<button type="button" onClick={() => saveRelays()}>
|
<button type="button" onClick={() => saveRelays()}>
|
||||||
Save
|
<FormattedMessage {...messages.Save} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{addRelay()}
|
{addRelay()}
|
||||||
|
60
src/Pages/settings/messages.js
Normal file
60
src/Pages/settings/messages.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { defineMessages } from "react-intl";
|
||||||
|
import { addIdAndDefaultMessageToMessages } from "Util";
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
Profile: "Profile",
|
||||||
|
Relays: "Relays",
|
||||||
|
Owner: "Owner",
|
||||||
|
Software: "Software",
|
||||||
|
Contact: "Contact",
|
||||||
|
Supports: "Supports",
|
||||||
|
Remove: "Remove",
|
||||||
|
Preferences: "Preferences",
|
||||||
|
Donate: "Donate",
|
||||||
|
LogOut: "Log Out",
|
||||||
|
Theme: "Theme",
|
||||||
|
System: "System (Default)",
|
||||||
|
Light: "Light",
|
||||||
|
Dark: "Dark",
|
||||||
|
AutoloadMedia: "Automatically load media",
|
||||||
|
AutoloadMediaHelp:
|
||||||
|
"Media in posts will automatically be shown for selected people, otherwise only the link will show",
|
||||||
|
None: "None",
|
||||||
|
FollowsOnly: "Follows only",
|
||||||
|
All: "All",
|
||||||
|
ImgProxy: "Image proxy service",
|
||||||
|
ImgProxyHelp: "Use imgproxy to compress images",
|
||||||
|
ServiceUrl: "Service URL",
|
||||||
|
ServiceKey: "Service Key",
|
||||||
|
ServiceSalt: "Service Salt",
|
||||||
|
EnableReactions: "Enable reactions",
|
||||||
|
EnableReactionsHelp:
|
||||||
|
"Reactions will be shown on every page, if disabled no reactions will be shown",
|
||||||
|
ConfirmReposts: "Confirm Reposts",
|
||||||
|
ConfirmRepostsHelp: "Reposts need to be manually confirmed",
|
||||||
|
ShowLatest: "Automatically show latest notes",
|
||||||
|
ShowLatestHelp: "Notes will stream in real time into global and posts tab",
|
||||||
|
FileUpload: "File upload service",
|
||||||
|
FileUploadHelp: "Pick which upload service you want to upload attachments to",
|
||||||
|
Default: "(Default)",
|
||||||
|
DebugMenus: "Debug Menus",
|
||||||
|
DebugMenusHelp: `Shows "Copy ID" and "Copy Event JSON" in the context menu on each message`,
|
||||||
|
EditProfile: "Edit Profile",
|
||||||
|
About: "About",
|
||||||
|
LnAddress: "LN Address",
|
||||||
|
Avatar: "Avatar",
|
||||||
|
Banner: "Banner",
|
||||||
|
Edit: "Edit",
|
||||||
|
PrivateKey: "Your Private Key Is (do not share this with anyone)",
|
||||||
|
Add: "Add",
|
||||||
|
AddRelays: "Add Relays",
|
||||||
|
Name: "Name",
|
||||||
|
Profile: "Profile",
|
||||||
|
Website: "Website",
|
||||||
|
Save: "Save",
|
||||||
|
DisplayName: "Display name",
|
||||||
|
Buy: "Buy",
|
||||||
|
Nip05: "NIP-05",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default addIdAndDefaultMessageToMessages(messages, "Pages.settings");
|
17
src/Util.ts
17
src/Util.ts
@ -195,3 +195,20 @@ export function addIdAndDefaultMessageToMessages(
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dedupeByPubkey(events: TaggedRawEvent[]) {
|
||||||
|
const deduped = events.reduce(
|
||||||
|
({ list, seen }: { list: TaggedRawEvent[]; seen: Set<HexKey> }, ev) => {
|
||||||
|
if (seen.has(ev.pubkey)) {
|
||||||
|
return { list, seen };
|
||||||
|
}
|
||||||
|
seen.add(ev.pubkey);
|
||||||
|
return {
|
||||||
|
seen,
|
||||||
|
list: [...list, ev],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ list: [], seen: new Set([]) }
|
||||||
|
);
|
||||||
|
return deduped.list as TaggedRawEvent[];
|
||||||
|
}
|
||||||
|
@ -271,6 +271,7 @@ button.icon:hover {
|
|||||||
|
|
||||||
.btn-rnd {
|
.btn-rnd {
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 100%;
|
||||||
width: 21px;
|
width: 21px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,3 +1,168 @@
|
|||||||
{
|
{
|
||||||
"Pages.Login": "Login"
|
"Element.AccountPage": "account page",
|
||||||
|
"Element.AccountSupport": "Account Support",
|
||||||
|
"Element.ActivateNow": "Activate Now",
|
||||||
|
"Element.AddToProfile": "Add to Profile",
|
||||||
|
"Element.Back": "Back",
|
||||||
|
"Element.Block": "Block",
|
||||||
|
"Element.BlockCount": "{n} blocked",
|
||||||
|
"Element.BuyNow": "Buy Now",
|
||||||
|
"Element.Buying": "Buying {item}",
|
||||||
|
"Element.Cancel": "Cancel",
|
||||||
|
"Element.Comment": "Comment",
|
||||||
|
"Element.Confirm": "Confirm",
|
||||||
|
"Element.ConfirmDeletion": "Are you sure you want to delete {id}",
|
||||||
|
"Element.ConfirmRepost": "Are you sure you want to repost: {id}",
|
||||||
|
"Element.CopyID": "Copy ID",
|
||||||
|
"Element.CopyJSON": "Copy Event JSON",
|
||||||
|
"Element.Custom": "Custom",
|
||||||
|
"Element.Delete": "Delete",
|
||||||
|
"Element.Deleted": "Deleted",
|
||||||
|
"Element.DisalledLater": "name will be available later",
|
||||||
|
"Element.Disallowed": "name is blocked",
|
||||||
|
"Element.Dislike": "{n} Dislike",
|
||||||
|
"Element.Dislikes": "Dislikes ({n})",
|
||||||
|
"Element.Expired": "Expired",
|
||||||
|
"Element.FindMore": "Find out more info about {service} at {link}",
|
||||||
|
"Element.Follow": "Follow",
|
||||||
|
"Element.FollowAll": "Follow all",
|
||||||
|
"Element.FollowerCount": "{n} followers",
|
||||||
|
"Element.FollowingCount": "Follows {n}",
|
||||||
|
"Element.FollowsYou": "follows you",
|
||||||
|
"Element.GoTo": "Go to",
|
||||||
|
"Element.Invoice": "Lightning Invoice",
|
||||||
|
"Element.InvoiceFail": "Failed to load invoice",
|
||||||
|
"Element.JustNow": "Just now",
|
||||||
|
"Element.LNURLFail": "Failed to load LNURL service",
|
||||||
|
"Element.Likes": "Likes ({n})",
|
||||||
|
"Element.Loading": "Loading...",
|
||||||
|
"Element.Logout": "Logout",
|
||||||
|
"Element.Milliseconds": "{n} ms",
|
||||||
|
"Element.Mute": "Mute",
|
||||||
|
"Element.MuteAll": "Mute all",
|
||||||
|
"Element.MuteCount": "{n} muted",
|
||||||
|
"Element.MutedAuthor": "This author has been muted",
|
||||||
|
"Element.NewNip": "Your new NIP-05 handle is:",
|
||||||
|
"Element.NotAvailable": "Not available:",
|
||||||
|
"Element.NotePlaceholder": "What's on your mind?",
|
||||||
|
"Element.NoteToSelf": "Note to Self",
|
||||||
|
"Element.OpenWallet": "Open Wallet",
|
||||||
|
"Element.OrderPaid": "Order Paid!",
|
||||||
|
"Element.Others": " & {n} {n, plural, =1 {other} other {others}}",
|
||||||
|
"Element.OthersZapped": "{n, plural, =0 {} =1 {zapped} other {zapped}}",
|
||||||
|
"Element.Paid": "Paid",
|
||||||
|
"Element.Pay": "Pay",
|
||||||
|
"Element.PayInvoice": "Pay Invoice",
|
||||||
|
"Element.Reactions": "Reactions",
|
||||||
|
"Element.ReactionsCount": "Reactions ({n})",
|
||||||
|
"Element.Read": "Read",
|
||||||
|
"Element.Regex": "name has disallowed characters",
|
||||||
|
"Element.Registered": "name is registered",
|
||||||
|
"Element.Reply": "Reply",
|
||||||
|
"Element.Reposts": "Reposts ({n})",
|
||||||
|
"Element.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||||
|
"Element.SavePassword": "Please make sure to save the following password in order to manage your handle in the future",
|
||||||
|
"Element.Seconds": "{n} secs",
|
||||||
|
"Element.Send": "Send",
|
||||||
|
"Element.SendSats": "Send sats",
|
||||||
|
"Element.SendZap": "Send zap",
|
||||||
|
"Element.Share": "Share",
|
||||||
|
"Element.Show": "Show",
|
||||||
|
"Element.ShowLatest": "Show latest {n} notes",
|
||||||
|
"Element.ShowMore": "Show more",
|
||||||
|
"Element.ShowReplies": "Show replies",
|
||||||
|
"Element.ToTarget": "{action} to {target}",
|
||||||
|
"Element.TooLong": "name too long",
|
||||||
|
"Element.TooShort": "name too short",
|
||||||
|
"Element.TranslateTo": "Translate to {lang}",
|
||||||
|
"Element.TranslatedFrom": "Translated from {lang}",
|
||||||
|
"Element.TranslationFailed": "Translation failed",
|
||||||
|
"Element.Unblock": "Unblock",
|
||||||
|
"Element.Unfollow": "Unfollow",
|
||||||
|
"Element.UnknownEventKind": "Unknown event kind: {kind}",
|
||||||
|
"Element.Unmute": "Unmute",
|
||||||
|
"Element.Write": "Write",
|
||||||
|
"Element.ZapAmount": "Zap amount in sats",
|
||||||
|
"Element.ZapSats": "Zap {n} sats",
|
||||||
|
"Element.ZapTarget": "Zap {target} {n} sats",
|
||||||
|
"Element.Zapped": "zapped",
|
||||||
|
"Element.Zaps": "Zaps ({n})",
|
||||||
|
"Pages.AvoidImpersonators": "Prevent fake accounts from imitating you",
|
||||||
|
"Pages.Blocked": "Blocked",
|
||||||
|
"Pages.Conversations": "Conversations",
|
||||||
|
"Pages.EasierToFind": "Make your profile easier to find and share",
|
||||||
|
"Pages.Followers": "Followers",
|
||||||
|
"Pages.Following": "Following {n}",
|
||||||
|
"Pages.Follows": "Follows",
|
||||||
|
"Pages.Funding": "Fund developers and platforms providing NIP-05 verification services",
|
||||||
|
"Pages.GetVerified": "Get Verified",
|
||||||
|
"Pages.Global": "Global",
|
||||||
|
"Pages.Login": "Login",
|
||||||
|
"Pages.MarkAllRead": "Mark All Read",
|
||||||
|
"Pages.Messages": "Messages",
|
||||||
|
"Pages.Muted": "Muted",
|
||||||
|
"Pages.NewUsers": "New users page",
|
||||||
|
"Pages.Nip05": "NIP-05 is a DNS based verification spec which helps to validate you as a real user.",
|
||||||
|
"Pages.Nip05Pros": "Getting NIP-05 verified can help:",
|
||||||
|
"Pages.NoFollows": "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
|
||||||
|
"Pages.NostrPlebsNip": "Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices",
|
||||||
|
"Pages.Notes": "Notes",
|
||||||
|
"Pages.Posts": "Posts",
|
||||||
|
"Pages.Reactions": "Reactions",
|
||||||
|
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||||
|
"Pages.Search": "Search",
|
||||||
|
"Pages.SearchPlaceholder": "Search...",
|
||||||
|
"Pages.Settings": "Settings",
|
||||||
|
"Pages.SnortSocialNip": "Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!",
|
||||||
|
"Pages.Zaps": "Zaps",
|
||||||
|
"Pages.settings.About": "About",
|
||||||
|
"Pages.settings.Add": "Add",
|
||||||
|
"Pages.settings.AddRelays": "Add Relays",
|
||||||
|
"Pages.settings.All": "All",
|
||||||
|
"Pages.settings.AutoloadMedia": "Automatically load media",
|
||||||
|
"Pages.settings.AutoloadMediaHelp": "Media in posts will automatically be shown for selected people, otherwise only the link will show",
|
||||||
|
"Pages.settings.Avatar": "Avatar",
|
||||||
|
"Pages.settings.Banner": "Banner",
|
||||||
|
"Pages.settings.Buy": "Buy",
|
||||||
|
"Pages.settings.ConfirmReposts": "Confirm Reposts",
|
||||||
|
"Pages.settings.ConfirmRepostsHelp": "Reposts need to be manually confirmed",
|
||||||
|
"Pages.settings.Contact": "Contact",
|
||||||
|
"Pages.settings.Dark": "Dark",
|
||||||
|
"Pages.settings.DebugMenus": "Debug Menus",
|
||||||
|
"Pages.settings.DebugMenusHelp": "Shows \"Copy ID\" and \"Copy Event JSON\" in the context menu on each message",
|
||||||
|
"Pages.settings.Default": "(Default)",
|
||||||
|
"Pages.settings.DisplayName": "Display name",
|
||||||
|
"Pages.settings.Donate": "Donate",
|
||||||
|
"Pages.settings.Edit": "Edit",
|
||||||
|
"Pages.settings.EditProfile": "Edit Profile",
|
||||||
|
"Pages.settings.EnableReactions": "Enable reactions",
|
||||||
|
"Pages.settings.EnableReactionsHelp": "Reactions will be shown on every page, if disabled no reactions will be shown",
|
||||||
|
"Pages.settings.FileUpload": "File upload service",
|
||||||
|
"Pages.settings.FileUploadHelp": "Pick which upload service you want to upload attachments to",
|
||||||
|
"Pages.settings.FollowsOnly": "Follows only",
|
||||||
|
"Pages.settings.ImgProxy": "Image proxy service",
|
||||||
|
"Pages.settings.ImgProxyHelp": "Use imgproxy to compress images",
|
||||||
|
"Pages.settings.Light": "Light",
|
||||||
|
"Pages.settings.LnAddress": "LN Address",
|
||||||
|
"Pages.settings.LogOut": "Log Out",
|
||||||
|
"Pages.settings.Name": "Name",
|
||||||
|
"Pages.settings.Nip05": "NIP-05",
|
||||||
|
"Pages.settings.None": "None",
|
||||||
|
"Pages.settings.Owner": "Owner",
|
||||||
|
"Pages.settings.Preferences": "Preferences",
|
||||||
|
"Pages.settings.PrivateKey": "Your Private Key Is (do not share this with anyone)",
|
||||||
|
"Pages.settings.Profile": "Profile",
|
||||||
|
"Pages.settings.Relays": "Relays",
|
||||||
|
"Pages.settings.Remove": "Remove",
|
||||||
|
"Pages.settings.Save": "Save",
|
||||||
|
"Pages.settings.ServiceKey": "Service Key",
|
||||||
|
"Pages.settings.ServiceSalt": "Service Salt",
|
||||||
|
"Pages.settings.ServiceUrl": "Service URL",
|
||||||
|
"Pages.settings.ShowLatest": "Automatically show latest notes",
|
||||||
|
"Pages.settings.ShowLatestHelp": "Notes will stream in real time into global and posts tab",
|
||||||
|
"Pages.settings.Software": "Software",
|
||||||
|
"Pages.settings.Supports": "Supports",
|
||||||
|
"Pages.settings.System": "System (Default)",
|
||||||
|
"Pages.settings.Theme": "Theme",
|
||||||
|
"Pages.settings.Website": "Website"
|
||||||
}
|
}
|
@ -1,3 +1,168 @@
|
|||||||
{
|
{
|
||||||
"Pages.Login": "Acceso"
|
"Element.AccountPage": "página de perfil",
|
||||||
|
"Element.AccountSupport": "Soporte de Cuenta",
|
||||||
|
"Element.ActivateNow": "Activar Ahora",
|
||||||
|
"Element.AddToProfile": "Añadir a tu perfil",
|
||||||
|
"Element.Back": "Atrás",
|
||||||
|
"Element.Block": "Bloquear",
|
||||||
|
"Element.BlockCount": "{n} bloqueados",
|
||||||
|
"Element.BuyNow": "Comprar",
|
||||||
|
"Element.Buying": "Comprando {item}",
|
||||||
|
"Element.Cancel": "Cancelar",
|
||||||
|
"Element.Comment": "Comentario",
|
||||||
|
"Element.Confirm": "Confirmar",
|
||||||
|
"Element.ConfirmDeletion": "¿Estás seguro de que quieres eliminar {id}?",
|
||||||
|
"Element.ConfirmRepost": "¿Estás seguro de que quieres republicar {id}",
|
||||||
|
"Element.CopyID": "Copiar ID",
|
||||||
|
"Element.CopyJSON": "Copiar JSON",
|
||||||
|
"Element.Custom": "Personalizar",
|
||||||
|
"Element.Delete": "Eliminar",
|
||||||
|
"Element.Deleted": "Eliminado",
|
||||||
|
"Element.DisalledLater": "",
|
||||||
|
"Element.Disallowed": "el nombre no está permitido",
|
||||||
|
"Element.Dislike": "{n} No me gusta",
|
||||||
|
"Element.Dislikes": "No me gusta ({n})",
|
||||||
|
"Element.Expired": "Caducada",
|
||||||
|
"Element.FindMore": "Aprende más sobre {service} en {link}",
|
||||||
|
"Element.Follow": "Seguir",
|
||||||
|
"Element.FollowAll": "Seguir todos",
|
||||||
|
"Element.FollowerCount": "{n} seguidores",
|
||||||
|
"Element.FollowingCount": "Siguiendo a {n}",
|
||||||
|
"Element.FollowsYou": "te sigue",
|
||||||
|
"Element.GoTo": "Ir a",
|
||||||
|
"Element.Invoice": "Factura Lightning",
|
||||||
|
"Element.InvoiceFail": "Error al cargar factura",
|
||||||
|
"Element.JustNow": "Justo ahora",
|
||||||
|
"Element.LNURLFail": "Error al contactar con el servicio LNURL",
|
||||||
|
"Element.Likes": "Me gusta ({n})",
|
||||||
|
"Element.Loading": "Cargando...",
|
||||||
|
"Element.Logout": "Cerrar sesión",
|
||||||
|
"Element.Milliseconds": "{n} ms",
|
||||||
|
"Element.Mute": "Silenciar",
|
||||||
|
"Element.MuteAll": "Silenciar todos",
|
||||||
|
"Element.MuteCount": "{n} silenciados",
|
||||||
|
"Element.MutedAuthor": "Este autor ha sido silenciado",
|
||||||
|
"Element.NewNip": "Tu nuevo NIP-05 es:",
|
||||||
|
"Element.NotAvailable": "No disponible:",
|
||||||
|
"Element.NotePlaceholder": "¿Qué tienes en mente?",
|
||||||
|
"Element.NoteToSelf": "Mensajes guardados",
|
||||||
|
"Element.OpenWallet": "Abrir Wallet",
|
||||||
|
"Element.OrderPaid": "Orden pagada!",
|
||||||
|
"Element.Others": " y {n} {n, plural, =1 {otro} other {otros}}",
|
||||||
|
"Element.OthersZapped": "{n, plural, =0 {} =1 {zapeó} other {zapearon}}",
|
||||||
|
"Element.Paid": "Pagado",
|
||||||
|
"Element.Pay": "Pagar",
|
||||||
|
"Element.PayInvoice": "Pagar Factura",
|
||||||
|
"Element.Reactions": "Reacciones",
|
||||||
|
"Element.ReactionsCount": "Reacciones ({n})",
|
||||||
|
"Element.Read": "Leer",
|
||||||
|
"Element.Regex": "el nombre tiene caracteres inválidos",
|
||||||
|
"Element.Registered": "el nombre ya está registrado",
|
||||||
|
"Element.Reply": "Responder",
|
||||||
|
"Element.Reposts": "Reposts ({n})",
|
||||||
|
"Element.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||||
|
"Element.SavePassword": "Asegúrate de guardar ésta contraseña para gestionar tu NIP-05 en el futuro",
|
||||||
|
"Element.Seconds": "{n} seg",
|
||||||
|
"Element.Send": "Enviar",
|
||||||
|
"Element.SendSats": "Enviar sats",
|
||||||
|
"Element.SendZap": "Enviar zap",
|
||||||
|
"Element.Share": "Compartir",
|
||||||
|
"Element.Show": "Mostrar",
|
||||||
|
"Element.ShowLatest": "Mostrar últimas {n} notas",
|
||||||
|
"Element.ShowMore": "Mostrar más",
|
||||||
|
"Element.ShowReplies": "Mostrar respuestas",
|
||||||
|
"Element.ToTarget": "{action} a {target}",
|
||||||
|
"Element.TooLong": "el nombre es demasiado largo",
|
||||||
|
"Element.TooShort": "el nombre es demasiado corto",
|
||||||
|
"Element.TranslateTo": "Traducir a {lang}",
|
||||||
|
"Element.TranslatedFrom": "Traducido de {lang}",
|
||||||
|
"Element.TranslationFailed": "Traducción fallida",
|
||||||
|
"Element.Unblock": "Desbloquear",
|
||||||
|
"Element.Unfollow": "No seguir",
|
||||||
|
"Element.UnknownEventKind": "Evento de tipo desconocido {kind}",
|
||||||
|
"Element.Unmute": "Desilenciar",
|
||||||
|
"Element.Write": "Escribir",
|
||||||
|
"Element.ZapAmount": "Total en sats",
|
||||||
|
"Element.ZapSats": "Zapear {n} sats",
|
||||||
|
"Element.ZapTarget": "Zapear {target} {n} sats",
|
||||||
|
"Element.Zapped": "zapeó",
|
||||||
|
"Element.Zaps": "Zaps ({n})",
|
||||||
|
"Pages.AvoidImpersonators": "",
|
||||||
|
"Pages.Blocked": "Bloqueados",
|
||||||
|
"Pages.Conversations": "Conversaciones",
|
||||||
|
"Pages.EasierToFind": "Haz tu perfil más fácil de encontrar y compartir",
|
||||||
|
"Pages.Followers": "Seguidores",
|
||||||
|
"Pages.Following": "Siguiendo ({n})",
|
||||||
|
"Pages.Follows": "Siguiendo",
|
||||||
|
"Pages.Funding": "Apoya a las plataformas y desarrolladores que proporcionan servicios de verificación",
|
||||||
|
"Pages.GetVerified": "Verifica tu perfil",
|
||||||
|
"Pages.Global": "Global",
|
||||||
|
"Pages.Login": "Acceso",
|
||||||
|
"Pages.MarkAllRead": "Marcar todo como leído",
|
||||||
|
"Pages.Messages": "Mensajes",
|
||||||
|
"Pages.Muted": "Silenciados",
|
||||||
|
"Pages.NewUsers": "Página de nuevos usuarios",
|
||||||
|
"Pages.Nip05": "NIP-05 es un sistema de verification basado en DNS que permite verificarte como un usuario real.",
|
||||||
|
"Pages.Nip05Pros": "Obtener un NIP-05 ayuda a:",
|
||||||
|
"Pages.NoFollows": "Hmmm no hay nada que mostrarte aquí... Echa un vistazo a la {newUsersPage} para seguir a algunas cuentas recomendadas!",
|
||||||
|
"Pages.NostrPlebsNip": "Nostr Plbes es uno de los primeros proveedores de NIP-05 y ofrece una buena colección de dominios a precios razonables.",
|
||||||
|
"Pages.Notes": "Notas",
|
||||||
|
"Pages.Posts": "Notas",
|
||||||
|
"Pages.Reactions": "Reacciones",
|
||||||
|
"Pages.Sats": "{n} {n, plural, =1 {sat} other {sats}}",
|
||||||
|
"Pages.Search": "Búsqueda",
|
||||||
|
"Pages.SearchPlaceholder": "Buscar...",
|
||||||
|
"Pages.Settings": "Configuración",
|
||||||
|
"Pages.SnortSocialNip": "Nuestro servicio de verificación NIP-05, apoya el desarrollo de este proyecto y obtén una apariencia especial en nuestra web!",
|
||||||
|
"Pages.Zaps": "Zaps",
|
||||||
|
"Pages.settings.About": "Bio",
|
||||||
|
"Pages.settings.Add": "Añadir",
|
||||||
|
"Pages.settings.AddRelays": "Añadir Relays",
|
||||||
|
"Pages.settings.All": "Todos",
|
||||||
|
"Pages.settings.AutoloadMedia": "Cargar medios automáticamente",
|
||||||
|
"Pages.settings.AutoloadMediaHelp": "Se cargarán las imágenes y vídeos automáticamente",
|
||||||
|
"Pages.settings.Avatar": "Avatar",
|
||||||
|
"Pages.settings.Banner": "Banner",
|
||||||
|
"Pages.settings.Buy": "Comprar",
|
||||||
|
"Pages.settings.ConfirmReposts": "Confirmar reposts",
|
||||||
|
"Pages.settings.ConfirmRepostsHelp": "",
|
||||||
|
"Pages.settings.Contact": "Contacto",
|
||||||
|
"Pages.settings.Dark": "Oscuro",
|
||||||
|
"Pages.settings.DebugMenus": "Menús de desarrolladores",
|
||||||
|
"Pages.settings.DebugMenusHelp": "Se mostrarán opciones adicionales para desarrolladores en el menú de las notas",
|
||||||
|
"Pages.settings.Default": "(Por defecto)",
|
||||||
|
"Pages.settings.DisplayName": "Nombre",
|
||||||
|
"Pages.settings.Donate": "Donar",
|
||||||
|
"Pages.settings.Edit": "Editar",
|
||||||
|
"Pages.settings.EditProfile": "Editar Perfil",
|
||||||
|
"Pages.settings.EnableReactions": "Activar reacciones",
|
||||||
|
"Pages.settings.EnableReactionsHelp": "Se mostrarán las reacciones a las notas",
|
||||||
|
"Pages.settings.FileUpload": "Subida de ficheros",
|
||||||
|
"Pages.settings.FileUploadHelp": "Elige qué servicio de subida de ficheros quieres utilizar",
|
||||||
|
"Pages.settings.FollowsOnly": "Sólo seguidos",
|
||||||
|
"Pages.settings.ImgProxy": "Proxy de imágines",
|
||||||
|
"Pages.settings.ImgProxyHelp": "",
|
||||||
|
"Pages.settings.Light": "Claro",
|
||||||
|
"Pages.settings.LnAddress": "Dirección Lightning",
|
||||||
|
"Pages.settings.LogOut": "Salir",
|
||||||
|
"Pages.settings.Name": "Nombre",
|
||||||
|
"Pages.settings.Nip05": "NIP-05",
|
||||||
|
"Pages.settings.None": "Ninguno",
|
||||||
|
"Pages.settings.Owner": "Dueño",
|
||||||
|
"Pages.settings.Preferences": "Preferencias",
|
||||||
|
"Pages.settings.PrivateKey": "Tu Clave Privada (no la compartas con nadie) es",
|
||||||
|
"Pages.settings.Profile": "Perfil",
|
||||||
|
"Pages.settings.Relays": "Relays",
|
||||||
|
"Pages.settings.Remove": "Eliminar",
|
||||||
|
"Pages.settings.Save": "Guardar",
|
||||||
|
"Pages.settings.ServiceKey": "Clave del Servicio",
|
||||||
|
"Pages.settings.ServiceSalt": "Salt del Servicio",
|
||||||
|
"Pages.settings.ServiceUrl": "URL del Servicio",
|
||||||
|
"Pages.settings.ShowLatest": "Mostrar notas nuevas automáticamente",
|
||||||
|
"Pages.settings.ShowLatestHelp": "Las notas nuevas se mostrarán automáticamente en tu línea de tiempo",
|
||||||
|
"Pages.settings.Software": "Software",
|
||||||
|
"Pages.settings.Supports": "Soporta",
|
||||||
|
"Pages.settings.System": "Sistema (por defecto)",
|
||||||
|
"Pages.settings.Theme": "Tema",
|
||||||
|
"Pages.settings.Website": "Web"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user