feat: translations
This commit is contained in:
@ -1,13 +1,17 @@
|
||||
import "./BackButton.css";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import ArrowBack from "Icons/ArrowBack";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface BackButtonProps {
|
||||
text?: string;
|
||||
onClick?(): void;
|
||||
}
|
||||
|
||||
const BackButton = ({ text = "Back", onClick }: BackButtonProps) => {
|
||||
const BackButton = ({ text, onClick }: BackButtonProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const onClickHandler = () => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
@ -17,7 +21,7 @@ const BackButton = ({ text = "Back", onClick }: BackButtonProps) => {
|
||||
return (
|
||||
<button className="back-button" type="button" onClick={onClickHandler}>
|
||||
<ArrowBack />
|
||||
{text}
|
||||
{text || formatMessage(messages.Back)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "Nostr";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface BlockButtonProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
@ -9,11 +12,11 @@ const BlockButton = ({ pubkey }: BlockButtonProps) => {
|
||||
const { block, unblock, isBlocked } = useModeration();
|
||||
return isBlocked(pubkey) ? (
|
||||
<button className="secondary" type="button" onClick={() => unblock(pubkey)}>
|
||||
Unblock
|
||||
<FormattedMessage {...messages.Unblock} />
|
||||
</button>
|
||||
) : (
|
||||
<button className="secondary" type="button" onClick={() => block(pubkey)}>
|
||||
Block
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { HexKey } from "Nostr";
|
||||
import type { RootState } from "State/Store";
|
||||
@ -9,6 +10,8 @@ import ProfilePreview from "Element/ProfilePreview";
|
||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface BlockListProps {
|
||||
variant: "muted" | "blocked";
|
||||
}
|
||||
@ -21,7 +24,12 @@ export default function BlockList({ variant }: BlockListProps) {
|
||||
<div className="main-content">
|
||||
{variant === "muted" && (
|
||||
<>
|
||||
<h4>{muted.length} muted</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.MuteCount}
|
||||
values={{ n: muted.length }}
|
||||
/>
|
||||
</h4>
|
||||
{muted.map((a) => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
@ -36,7 +44,12 @@ export default function BlockList({ variant }: BlockListProps) {
|
||||
)}
|
||||
{variant === "blocked" && (
|
||||
<>
|
||||
<h4>{blocked.length} blocked</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.BlockCount}
|
||||
values={{ n: blocked.length }}
|
||||
/>
|
||||
</h4>
|
||||
{blocked.map((a) => {
|
||||
return (
|
||||
<ProfilePreview
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "./DM.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
@ -12,6 +13,8 @@ import { RootState } from "State/Store";
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import { incDmInteraction } from "State/Login";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export type DMProps = {
|
||||
data: TaggedRawEvent;
|
||||
};
|
||||
@ -25,6 +28,7 @@ export default function DM(props: DMProps) {
|
||||
const [content, setContent] = useState("Loading...");
|
||||
const [decrypted, setDecrypted] = useState(false);
|
||||
const { ref, inView } = useInView();
|
||||
const { formatMessage } = useIntl();
|
||||
const isMe = props.data.pubkey === pubKey;
|
||||
const otherPubkey = isMe
|
||||
? pubKey
|
||||
@ -50,7 +54,10 @@ export default function DM(props: DMProps) {
|
||||
return (
|
||||
<div className={`flex dm f-col${isMe ? " me" : ""}`} ref={ref}>
|
||||
<div>
|
||||
<NoteTime from={props.data.created_at * 1000} fallback={"Just now"} />
|
||||
<NoteTime
|
||||
from={props.data.created_at * 1000}
|
||||
fallback={formatMessage(messages.JustNow)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<Text
|
||||
|
@ -1,12 +1,13 @@
|
||||
import "./FollowButton.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
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 { RootState } from "State/Store";
|
||||
import { parseId } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface FollowButtonProps {
|
||||
pubkey: HexKey;
|
||||
className?: string;
|
||||
@ -35,7 +36,11 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
className={isFollowing ? `${baseClassname} secondary` : baseClassname}
|
||||
onClick={() => (isFollowing ? unfollow(pubkey) : follow(pubkey))}
|
||||
>
|
||||
{isFollowing ? "Unfollow" : "Follow"}
|
||||
{isFollowing ? (
|
||||
<FormattedMessage {...messages.Unfollow} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Follow} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { HexKey } from "Nostr";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
title?: string;
|
||||
@ -26,7 +30,7 @@ export default function FollowListBase({
|
||||
type="button"
|
||||
onClick={() => followAll()}
|
||||
>
|
||||
Follow All
|
||||
<FormattedMessage {...messages.FollowAll} />
|
||||
</button>
|
||||
</div>
|
||||
{pubkeys?.map((a) => (
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import useFollowersFeed from "Feed/FollowersFeed";
|
||||
import { HexKey } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import FollowListBase from "Element/FollowListBase";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface FollowersListProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
|
||||
export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const feed = useFollowersFeed(pubkey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
@ -18,9 +23,12 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||
a.tags.some((b) => b[0] === "p" && b[1] === pubkey)
|
||||
);
|
||||
return [...new Set(contactLists?.map((a) => a.pubkey))];
|
||||
}, [feed]);
|
||||
}, [feed, pubkey]);
|
||||
|
||||
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 { useIntl } from "react-intl";
|
||||
|
||||
import useFollowsFeed from "Feed/FollowsFeed";
|
||||
import { HexKey } from "Nostr";
|
||||
import FollowListBase from "Element/FollowListBase";
|
||||
import { getFollowers } from "Feed/FollowsFeed";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface FollowsListProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
|
||||
export default function FollowsList({ pubkey }: FollowsListProps) {
|
||||
const feed = useFollowsFeed(pubkey);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed]);
|
||||
}, [feed, pubkey]);
|
||||
|
||||
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 { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { HexKey } from "Nostr";
|
||||
import { RootState } from "State/Store";
|
||||
import useFollowsFeed from "Feed/FollowsFeed";
|
||||
import { getFollowers } from "Feed/FollowsFeed";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface FollowsYouProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
|
||||
export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const feed = useFollowsFeed(pubkey);
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
@ -18,11 +23,11 @@ export default function FollowsYou({ pubkey }: FollowsYouProps) {
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed]);
|
||||
}, [feed, pubkey]);
|
||||
|
||||
const followsMe = pubkeys.includes(loginPubKey!) ?? false;
|
||||
|
||||
return (
|
||||
<>{followsMe ? <span className="follows-you">follows you</span> : null}</>
|
||||
);
|
||||
return followsMe ? (
|
||||
<span className="follows-you">{formatMessage(messages.FollowsYou)}</span>
|
||||
) : null;
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import "./Invoice.css";
|
||||
import { useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
// @ts-expect-error
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { useMemo } from "react";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import SendSats from "Element/SendSats";
|
||||
import ZapCircle from "Icons/ZapCircle";
|
||||
import useWebln from "Hooks/useWebln";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface InvoiceProps {
|
||||
invoice: string;
|
||||
}
|
||||
@ -15,6 +17,7 @@ export default function Invoice(props: InvoiceProps) {
|
||||
const invoice = props.invoice;
|
||||
const webln = useWebln();
|
||||
const [showInvoice, setShowInvoice] = useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const info = useMemo(() => {
|
||||
try {
|
||||
@ -55,10 +58,12 @@ export default function Invoice(props: InvoiceProps) {
|
||||
function header() {
|
||||
return (
|
||||
<>
|
||||
<h4>Lightning Invoice</h4>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.Invoice} />
|
||||
</h4>
|
||||
<ZapCircle className="zap-circle" />
|
||||
<SendSats
|
||||
title="Pay Invoice"
|
||||
title={formatMessage(messages.PayInvoice)}
|
||||
invoice={invoice}
|
||||
show={showInvoice}
|
||||
onClose={() => setShowInvoice(false)}
|
||||
@ -102,10 +107,16 @@ export default function Invoice(props: InvoiceProps) {
|
||||
<div className="invoice-body">
|
||||
{description && <p>{description}</p>}
|
||||
{isPaid ? (
|
||||
<div className="paid">Paid</div>
|
||||
<div className="paid">
|
||||
<FormattedMessage {...messages.Paid} />
|
||||
</div>
|
||||
) : (
|
||||
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
||||
{isExpired ? "Expired" : "Pay"}
|
||||
{isExpired ? (
|
||||
<FormattedMessage {...messages.Expired} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Pay} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export default function LoadMore({
|
||||
onLoadMore,
|
||||
shouldLoadMore,
|
||||
@ -28,7 +31,7 @@ export default function LoadMore({
|
||||
|
||||
return (
|
||||
<div ref={ref} className="mb10">
|
||||
{children ?? "Loading..."}
|
||||
{children ?? <FormattedMessage {...messages.Loading} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { logout } from "State/Login";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export default function LogoutButton() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@ -15,7 +18,7 @@ export default function LogoutButton() {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
<FormattedMessage {...messages.Logout} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "Nostr";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface MuteButtonProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
@ -9,11 +12,11 @@ const MuteButton = ({ pubkey }: MuteButtonProps) => {
|
||||
const { mute, unmute, isMuted } = useModeration();
|
||||
return isMuted(pubkey) ? (
|
||||
<button className="secondary" type="button" onClick={() => unmute(pubkey)}>
|
||||
Unmute
|
||||
<FormattedMessage {...messages.Unmute} />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={() => mute(pubkey)}>
|
||||
Mute
|
||||
<FormattedMessage {...messages.Mute} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { HexKey } from "Nostr";
|
||||
import type { RootState } from "State/Store";
|
||||
import MuteButton from "Element/MuteButton";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useMutedFeed, { getMuted } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface MutedListProps {
|
||||
pubkey: HexKey;
|
||||
}
|
||||
|
||||
export default function MutedList({ pubkey }: MutedListProps) {
|
||||
const { muted, isMuted, mute, unmute, muteAll } = useModeration();
|
||||
const { isMuted, muteAll } = useModeration();
|
||||
const feed = useMutedFeed(pubkey);
|
||||
const pubkeys = useMemo(() => {
|
||||
return getMuted(feed.store, pubkey);
|
||||
@ -23,14 +24,19 @@ export default function MutedList({ pubkey }: MutedListProps) {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<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
|
||||
disabled={hasAllMuted || pubkeys.length === 0}
|
||||
className="transparent"
|
||||
type="button"
|
||||
onClick={() => muteAll(pubkeys)}
|
||||
>
|
||||
Mute all
|
||||
<FormattedMessage {...messages.MuteAll} />
|
||||
</button>
|
||||
</div>
|
||||
{pubkeys?.map((a) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
@ -18,6 +19,8 @@ import useEventPublisher from "Feed/EventPublisher";
|
||||
import { debounce, hexToBech32 } from "Util";
|
||||
import { UserMetadata } from "Nostr";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
type Nip05ServiceProps = {
|
||||
name: string;
|
||||
service: URL | string;
|
||||
@ -30,6 +33,7 @@ type ReduxStore = any;
|
||||
|
||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
const navigate = useNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const pubkey = useSelector<ReduxStore, string>((s) => s.login.publicKey);
|
||||
const user = useUserProfile(pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
@ -129,12 +133,12 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
|
||||
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||
let whyMap = new Map([
|
||||
["TOO_SHORT", "name too short"],
|
||||
["TOO_LONG", "name too long"],
|
||||
["REGEX", "name has disallowed characters"],
|
||||
["REGISTERED", "name is registered"],
|
||||
["DISALLOWED_null", "name is blocked"],
|
||||
["DISALLOWED_later", "name will be available later"],
|
||||
["TOO_SHORT", formatMessage(messages.TooShort)],
|
||||
["TOO_LONG", formatMessage(messages.TooLong)],
|
||||
["REGEX", formatMessage(messages.Regex)],
|
||||
["REGISTERED", formatMessage(messages.Registered)],
|
||||
["DISALLOWED_null", formatMessage(messages.Disallowed)],
|
||||
["DISALLOWED_later", formatMessage(messages.DisalledLater)],
|
||||
]);
|
||||
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
||||
}
|
||||
@ -171,10 +175,17 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
<h3>{props.name}</h3>
|
||||
{props.about}
|
||||
<p>
|
||||
Find out more info about {props.name} at{" "}
|
||||
<a href={props.link} target="_blank" rel="noreferrer">
|
||||
{props.link}
|
||||
</a>
|
||||
<FormattedMessage
|
||||
{...messages.FindMore}
|
||||
values={{
|
||||
service: props.name,
|
||||
link: (
|
||||
<a href={props.link} target="_blank" rel="noreferrer">
|
||||
{props.link}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{error && <b className="error">{error.error}</b>}
|
||||
{!registerStatus && (
|
||||
@ -196,7 +207,10 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
{availabilityResponse?.available && !registerStatus && (
|
||||
<div className="flex">
|
||||
<div className="mr10">
|
||||
{availabilityResponse.quote?.price.toLocaleString()} sats
|
||||
<FormattedMessage
|
||||
{...messages.Sats}
|
||||
values={{ n: availabilityResponse.quote?.price }}
|
||||
/>
|
||||
<br />
|
||||
<small>{availabilityResponse.quote?.data.type}</small>
|
||||
</div>
|
||||
@ -208,14 +222,14 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
disabled
|
||||
/>
|
||||
<AsyncButton onClick={() => startBuy(handle, domain)}>
|
||||
Buy Now
|
||||
<FormattedMessage {...messages.BuyNow} />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
)}
|
||||
{availabilityResponse?.available === false && !registerStatus && (
|
||||
<div className="flex">
|
||||
<b className="error">
|
||||
Not available:{" "}
|
||||
<FormattedMessage {...messages.NotAvailable} />{" "}
|
||||
{mapError(
|
||||
availabilityResponse.why!,
|
||||
availabilityResponse.reasonTag || null
|
||||
@ -227,32 +241,37 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
invoice={registerResponse?.invoice}
|
||||
show={showInvoice}
|
||||
onClose={() => setShowInvoice(false)}
|
||||
title={`Buying ${handle}@${domain}`}
|
||||
title={formatMessage(messages.Buying, { item: `${handle}@${domain}` })}
|
||||
/>
|
||||
{registerStatus?.paid && (
|
||||
<div className="flex f-col">
|
||||
<h4>Order Paid!</h4>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.OrderPaid} />
|
||||
</h4>
|
||||
<p>
|
||||
Your new NIP-05 handle is:{" "}
|
||||
<FormattedMessage {...messages.NewNip} />{" "}
|
||||
<code>
|
||||
{handle}@{domain}
|
||||
</code>
|
||||
</p>
|
||||
<h3>Account Support</h3>
|
||||
<h3>
|
||||
<FormattedMessage {...messages.AccountSupport} />
|
||||
</h3>
|
||||
<p>
|
||||
Please make sure to save the following password in order to manage
|
||||
your handle in the future
|
||||
<FormattedMessage {...messages.SavePassword} />
|
||||
</p>
|
||||
<Copy text={registerStatus.password} />
|
||||
<p>
|
||||
Go to{" "}
|
||||
<FormattedMessage {...messages.GoTo} />{" "}
|
||||
<a href={props.supportLink} target="_blank" rel="noreferrer">
|
||||
account page
|
||||
<FormattedMessage {...messages.AccountPage} />
|
||||
</a>
|
||||
</p>
|
||||
<h4>Activate Now</h4>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.ActivateNow} />
|
||||
</h4>
|
||||
<AsyncButton onClick={() => updateProfile(handle, domain)}>
|
||||
Add to Profile
|
||||
<FormattedMessage {...messages.AddToProfile} />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
)}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
|
||||
import { default as NEvent } from "Nostr/Event";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
@ -22,6 +23,8 @@ import { useUserProfiles } from "Feed/ProfileFeed";
|
||||
import { TaggedRawEvent, u256 } from "Nostr";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface NoteProps {
|
||||
data?: TaggedRawEvent;
|
||||
className?: string;
|
||||
@ -43,8 +46,12 @@ const HiddenNote = ({ children }: any) => {
|
||||
) : (
|
||||
<div className="card note hidden-note">
|
||||
<div className="header">
|
||||
<p>This author has been muted</p>
|
||||
<button onClick={() => setShow(true)}>Show</button>
|
||||
<p>
|
||||
<FormattedMessage {...messages.MutedAuthor} />
|
||||
</p>
|
||||
<button onClick={() => setShow(true)}>
|
||||
<FormattedMessage {...messages.Show} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -76,6 +83,7 @@ export default function Note(props: NoteProps) {
|
||||
const baseClassname = `note card ${props.className ? props.className : ""}`;
|
||||
const [translated, setTranslated] = useState<Translation>();
|
||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const options = {
|
||||
showHeader: true,
|
||||
@ -87,7 +95,11 @@ export default function Note(props: NoteProps) {
|
||||
const transformBody = useCallback(() => {
|
||||
let body = ev?.Content ?? "";
|
||||
if (deletions?.length > 0) {
|
||||
return <b className="error">Deleted</b>;
|
||||
return (
|
||||
<b className="error">
|
||||
<FormattedMessage {...messages.Deleted} />
|
||||
</b>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
@ -157,7 +169,7 @@ export default function Note(props: NoteProps) {
|
||||
: mentions?.map(renderMention);
|
||||
const others =
|
||||
mentions.length > maxMentions
|
||||
? ` & ${othersLength} other${othersLength > 1 ? "s" : ""}`
|
||||
? formatMessage(messages.Others, { n: othersLength })
|
||||
: "";
|
||||
return (
|
||||
<div className="reply">
|
||||
@ -181,7 +193,12 @@ export default function Note(props: NoteProps) {
|
||||
if (ev.Kind !== EventKind.TextNote) {
|
||||
return (
|
||||
<>
|
||||
<h4>Unknown event kind: {ev.Kind}</h4>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
{...messages.UnknownEventKind}
|
||||
values={{ kind: ev.Kind }}
|
||||
/>
|
||||
</h4>
|
||||
<pre>{JSON.stringify(ev.ToObject(), undefined, " ")}</pre>
|
||||
</>
|
||||
);
|
||||
@ -192,13 +209,20 @@ export default function Note(props: NoteProps) {
|
||||
return (
|
||||
<>
|
||||
<p className="highlight">
|
||||
Translated from {translated.fromLanguage}:
|
||||
<FormattedMessage
|
||||
{...messages.TranslatedFrom}
|
||||
values={{ lang: translated.fromLanguage }}
|
||||
/>
|
||||
</p>
|
||||
{translated.text}
|
||||
</>
|
||||
);
|
||||
} 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;
|
||||
return (
|
||||
<>
|
||||
{options.showHeader ? (
|
||||
{options.showHeader && (
|
||||
<div className="header flex">
|
||||
<ProfileImage
|
||||
pubkey={ev.RootPubKey}
|
||||
subHeader={replyTag() ?? undefined}
|
||||
/>
|
||||
{options.showTime ? (
|
||||
{options.showTime && (
|
||||
<div className="info">
|
||||
<NoteTime from={ev.CreatedAt * 1000} />
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||
{transformBody()}
|
||||
{translation()}
|
||||
@ -228,7 +252,7 @@ export default function Note(props: NoteProps) {
|
||||
className="expand-note mt10 flex f-center"
|
||||
onClick={() => setShowMore(true)}
|
||||
>
|
||||
Show more
|
||||
<FormattedMessage {...messages.ShowMore} />
|
||||
</span>
|
||||
)}
|
||||
{options.showFooter && (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "./NoteCreator.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import Attachment from "Icons/Attachment";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { openFile } from "Util";
|
||||
@ -10,6 +11,8 @@ import ProfileImage from "Element/ProfileImage";
|
||||
import { default as NEvent } from "Nostr/Event";
|
||||
import useFileUpload from "Upload";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface NotePreviewProps {
|
||||
note: NEvent;
|
||||
}
|
||||
@ -121,10 +124,14 @@ export function NoteCreator(props: NoteCreatorProps) {
|
||||
</div>
|
||||
<div className="note-creator-actions">
|
||||
<button className="secondary" type="button" onClick={cancel}>
|
||||
Cancel
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button type="button" onClick={onSubmit}>
|
||||
{replyTo ? "Reply" : "Send"}
|
||||
{replyTo ? (
|
||||
<FormattedMessage {...messages.Reply} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.Send} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import {
|
||||
faTrash,
|
||||
faHeart,
|
||||
faRepeat,
|
||||
faShareNodes,
|
||||
faCopy,
|
||||
@ -19,10 +21,17 @@ import Zap from "Icons/Zap";
|
||||
import Reply from "Icons/Reply";
|
||||
import { formatShort } from "Number";
|
||||
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 Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { parseZap, ZapsSummary } from "Element/Zap";
|
||||
import { parseZap, ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { default as NEvent } from "Nostr/Event";
|
||||
import { RootState } from "State/Store";
|
||||
@ -32,6 +41,8 @@ import { UserPreferences } from "State/Login";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { TranslateHost } from "Const";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface Translation {
|
||||
text: string;
|
||||
fromLanguage: string;
|
||||
@ -46,7 +57,7 @@ export interface NoteFooterProps {
|
||||
|
||||
export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { related, ev } = props;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const login = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
@ -57,6 +68,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const author = useUserProfile(ev.RootPubKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const [tip, setTip] = useState(false);
|
||||
const isMine = ev.RootPubKey === login;
|
||||
const lang = window.navigator.language;
|
||||
@ -68,31 +80,40 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
[related, ev]
|
||||
);
|
||||
const reposts = useMemo(
|
||||
() => getReactions(related, ev.Id, EventKind.Repost),
|
||||
() => dedupeByPubkey(getReactions(related, ev.Id, EventKind.Repost)),
|
||||
[related, ev]
|
||||
);
|
||||
const zaps = useMemo(
|
||||
() =>
|
||||
getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||
.map(parseZap)
|
||||
.filter((z) => z.valid && z.zapper !== ev.PubKey),
|
||||
[related]
|
||||
);
|
||||
const zaps = useMemo(() => {
|
||||
const sortedZaps = getReactions(related, ev.Id, EventKind.ZapReceipt)
|
||||
.map(parseZap)
|
||||
.filter((z) => z.valid && z.zapper !== ev.PubKey);
|
||||
sortedZaps.sort((a, b) => b.amount - a.amount);
|
||||
return sortedZaps;
|
||||
}, [related]);
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = zaps.some((a) => a.zapper === login);
|
||||
const groupReactions = useMemo(() => {
|
||||
return reactions?.reduce(
|
||||
(acc, { content }) => {
|
||||
let r = normalizeReaction(content);
|
||||
const amount = acc[r] || 0;
|
||||
return { ...acc, [r]: amount + 1 };
|
||||
const result = reactions?.reduce(
|
||||
(acc, reaction) => {
|
||||
let kind = normalizeReaction(reaction.content);
|
||||
const rs = acc[kind] || [];
|
||||
if (rs.map((e) => e.pubkey).includes(reaction.pubkey)) {
|
||||
return acc;
|
||||
}
|
||||
return { ...acc, [kind]: [...rs, reaction] };
|
||||
},
|
||||
{
|
||||
[Reaction.Positive]: 0,
|
||||
[Reaction.Negative]: 0,
|
||||
[Reaction.Positive]: [] as TaggedRawEvent[],
|
||||
[Reaction.Negative]: [] as TaggedRawEvent[],
|
||||
}
|
||||
);
|
||||
return {
|
||||
[Reaction.Positive]: dedupeByPubkey(result[Reaction.Positive]),
|
||||
[Reaction.Negative]: dedupeByPubkey(result[Reaction.Negative]),
|
||||
};
|
||||
}, [reactions]);
|
||||
const positive = groupReactions[Reaction.Positive];
|
||||
const negative = groupReactions[Reaction.Negative];
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return reactions?.some(
|
||||
@ -115,7 +136,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
async function deleteEvent() {
|
||||
if (
|
||||
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);
|
||||
@ -127,7 +148,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
if (!hasReposted()) {
|
||||
if (
|
||||
!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);
|
||||
publisher.broadcast(evRepost);
|
||||
@ -191,7 +212,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
<Heart />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(groupReactions[Reaction.Positive])}
|
||||
{formatShort(positive.length)}
|
||||
</div>
|
||||
</div>
|
||||
{repostIcon()}
|
||||
@ -250,42 +271,53 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
return (
|
||||
<>
|
||||
{prefs.enableReactions && (
|
||||
<MenuItem onClick={() => react("-")}>
|
||||
<Dislike />
|
||||
{formatShort(groupReactions[Reaction.Negative])}
|
||||
Dislike
|
||||
<MenuItem onClick={() => setShowReactions(true)}>
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
<FormattedMessage {...messages.Reactions} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => share()}>
|
||||
<FontAwesomeIcon icon={faShareNodes} />
|
||||
Share
|
||||
<FormattedMessage {...messages.Share} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => copyId()}>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
Copy ID
|
||||
<FormattedMessage {...messages.CopyID} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => mute(ev.PubKey)}>
|
||||
<FontAwesomeIcon icon={faCommentSlash} />
|
||||
Mute
|
||||
<FormattedMessage {...messages.Mute} />
|
||||
</MenuItem>
|
||||
{prefs.enableReactions && (
|
||||
<MenuItem onClick={() => react("-")}>
|
||||
<Dislike />
|
||||
<FormattedMessage
|
||||
{...messages.Dislike}
|
||||
values={{ n: negative.length }}
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => block(ev.PubKey)}>
|
||||
<FontAwesomeIcon icon={faBan} />
|
||||
Block
|
||||
<FormattedMessage {...messages.Block} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => translate()}>
|
||||
<FontAwesomeIcon icon={faLanguage} />
|
||||
Translate to {langNames.of(lang.split("-")[0])}
|
||||
<FormattedMessage
|
||||
{...messages.TranslateTo}
|
||||
values={{ lang: langNames.of(lang.split("-")[0]) }}
|
||||
/>
|
||||
</MenuItem>
|
||||
{prefs.showDebugMenus && (
|
||||
<MenuItem onClick={() => copyEvent()}>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
Copy Event JSON
|
||||
<FormattedMessage {...messages.CopyJSON} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{isMine && (
|
||||
<MenuItem onClick={() => deleteEvent()}>
|
||||
<FontAwesomeIcon icon={faTrash} className="red" />
|
||||
Delete
|
||||
<FormattedMessage {...messages.Delete} />
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
@ -326,6 +358,14 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
show={reply}
|
||||
setShow={setReply}
|
||||
/>
|
||||
<Reactions
|
||||
show={showReactions}
|
||||
setShow={setShowReactions}
|
||||
positive={positive}
|
||||
negative={negative}
|
||||
reposts={reposts}
|
||||
zaps={zaps}
|
||||
/>
|
||||
<SendSats
|
||||
svc={author?.lud16 || author?.lud06}
|
||||
onClose={() => setTip(false)}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedRelativeTime } from "react-intl";
|
||||
|
||||
const MinuteInMs = 1_000 * 60;
|
||||
const HourInMs = MinuteInMs * 60;
|
||||
@ -16,12 +17,12 @@ export default function NoteTime(props: NoteTimeProps) {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "long",
|
||||
}).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() {
|
||||
let fromDate = new Date(from);
|
||||
let ago = new Date().getTime() - from;
|
||||
let absAgo = Math.abs(ago);
|
||||
if (absAgo > DayInMs) {
|
||||
return fromDate.toLocaleDateString(undefined, {
|
||||
year: "2-digit",
|
||||
|
@ -1,12 +1,14 @@
|
||||
import "./NoteToSelf.css";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { profileLink } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface NoteToSelfProps {
|
||||
pubkey: string;
|
||||
clickable?: boolean;
|
||||
@ -18,7 +20,8 @@ function NoteLabel({ pubkey, link }: NoteToSelfProps) {
|
||||
const user = useUserProfile(pubkey);
|
||||
return (
|
||||
<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} />}
|
||||
</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 { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
faPlug,
|
||||
faSquareCheck,
|
||||
@ -15,7 +16,8 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRelays } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface RelayProps {
|
||||
addr: string;
|
||||
@ -23,6 +25,7 @@ export interface RelayProps {
|
||||
|
||||
export default function Relay(props: RelayProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const allRelaySettings = useSelector<
|
||||
RootState,
|
||||
@ -55,7 +58,7 @@ export default function Relay(props: RelayProps) {
|
||||
<div className="flex mb10">
|
||||
<b className="f-2">{name}</b>
|
||||
<div className="f-1">
|
||||
Write
|
||||
<FormattedMessage {...messages.Write} />
|
||||
<span
|
||||
className="checkmark"
|
||||
onClick={() =>
|
||||
@ -71,7 +74,7 @@ export default function Relay(props: RelayProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div className="f-1">
|
||||
Read
|
||||
<FormattedMessage {...messages.Read} />
|
||||
<span
|
||||
className="checkmark"
|
||||
onClick={() =>
|
||||
@ -91,8 +94,12 @@ export default function Relay(props: RelayProps) {
|
||||
<div className="f-grow">
|
||||
<FontAwesomeIcon icon={faWifi} />{" "}
|
||||
{latency > 2000
|
||||
? `${(latency / 1000).toFixed(0)} secs`
|
||||
: `${latency.toLocaleString()} ms`}
|
||||
? formatMessage(messages.Seconds, {
|
||||
n: (latency / 1000).toFixed(0),
|
||||
})
|
||||
: formatMessage(messages.Milliseconds, {
|
||||
n: latency.toLocaleString(),
|
||||
})}
|
||||
|
||||
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
|
||||
</div>
|
||||
|
@ -61,15 +61,6 @@
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
.lnurl-tip .btn {
|
||||
background-color: inherit;
|
||||
width: 210px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.lnurl-tip .btn:hover {
|
||||
}
|
||||
|
||||
.amounts {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "./SendSats.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import { bech32ToText } from "Util";
|
||||
@ -15,6 +16,8 @@ import Copy from "Element/Copy";
|
||||
import useWebln from "Hooks/useWebln";
|
||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface LNURLService {
|
||||
nostrPubkey?: HexKey;
|
||||
minSendable?: number;
|
||||
@ -71,6 +74,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||
const webln = useWebln(show);
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
|
||||
@ -78,7 +82,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
if (show && !props.invoice) {
|
||||
loadService()
|
||||
.then((a) => setPayService(a!))
|
||||
.catch(() => setError("Failed to load LNURL service"));
|
||||
.catch(() => setError(formatMessage(messages.LNURLFail)));
|
||||
} else {
|
||||
setPayService(undefined);
|
||||
setError(undefined);
|
||||
@ -170,10 +174,10 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
payWebLNIfEnabled(data);
|
||||
}
|
||||
} else {
|
||||
setError("Failed to load invoice");
|
||||
setError(formatMessage(messages.InvoiceFail));
|
||||
}
|
||||
} catch (e) {
|
||||
setError("Failed to load invoice");
|
||||
setError(formatMessage(messages.InvoiceFail));
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,7 +191,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
min={min}
|
||||
max={max}
|
||||
className="f-grow mr10"
|
||||
placeholder="Custom"
|
||||
placeholder={formatMessage(messages.Custom)}
|
||||
value={customAmount}
|
||||
onChange={(e) => setCustomAmount(parseInt(e.target.value))}
|
||||
/>
|
||||
@ -197,7 +201,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
disabled={!Boolean(customAmount)}
|
||||
onClick={() => selectAmount(customAmount!)}
|
||||
>
|
||||
Confirm
|
||||
<FormattedMessage {...messages.Confirm} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@ -220,7 +224,9 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
if (invoice) return null;
|
||||
return (
|
||||
<>
|
||||
<h3>Zap amount in sats</h3>
|
||||
<h3>
|
||||
<FormattedMessage {...messages.ZapAmount} />
|
||||
</h3>
|
||||
<div className="amounts" ref={horizontalScroll}>
|
||||
{serviceAmounts.map((a) => (
|
||||
<span
|
||||
@ -235,15 +241,16 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
</div>
|
||||
{payService && custom()}
|
||||
<div className="flex">
|
||||
{(payService?.commentAllowed ?? 0) > 0 && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Comment"
|
||||
className="f-grow"
|
||||
maxLength={payService?.commentAllowed}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{(payService?.commentAllowed ?? 0) > 0 ||
|
||||
(payService?.nostrPubkey && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={formatMessage(messages.Comment)}
|
||||
className="f-grow"
|
||||
maxLength={payService?.commentAllowed || 120}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{(amount ?? 0) > 0 && (
|
||||
<button
|
||||
@ -252,9 +259,18 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
onClick={() => loadInvoice()}
|
||||
>
|
||||
<div className="zap-action-container">
|
||||
<Zap /> Zap
|
||||
{target && ` ${target} `}
|
||||
{formatShort(amount)} sats
|
||||
<Zap />
|
||||
{target ? (
|
||||
<FormattedMessage
|
||||
{...messages.ZapTarget}
|
||||
values={{ target, n: formatShort(amount) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
{...messages.ZapSats}
|
||||
values={{ n: formatShort(amount) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
@ -281,7 +297,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
type="button"
|
||||
onClick={() => window.open(`lightning:${pr}`)}
|
||||
>
|
||||
Open Wallet
|
||||
<FormattedMessage {...messages.OpenWallet} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@ -297,7 +313,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
<div className="success-action">
|
||||
<p className="paid">
|
||||
<Check className="success mr10" />
|
||||
{success?.description ?? "Paid!"}
|
||||
{success?.description ?? <FormattedMessage {...messages.Paid} />}
|
||||
</p>
|
||||
{success.url && (
|
||||
<p>
|
||||
@ -310,8 +326,15 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const defaultTitle = payService?.nostrPubkey ? "Send zap" : "Send sats";
|
||||
const title = target ? `${defaultTitle} to ${target}` : defaultTitle;
|
||||
const defaultTitle = payService?.nostrPubkey
|
||||
? formatMessage(messages.SendZap)
|
||||
: formatMessage(messages.SendSats);
|
||||
const title = target
|
||||
? formatMessage(messages.ToTarget, {
|
||||
action: defaultTitle,
|
||||
target,
|
||||
})
|
||||
: defaultTitle;
|
||||
if (!show) return null;
|
||||
return (
|
||||
<Modal className="lnurl-modal" onClose={onClose}>
|
||||
|
@ -1,4 +1,7 @@
|
||||
import "./ShowMore.css";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface ShowMoreProps {
|
||||
text?: string;
|
||||
@ -6,16 +9,14 @@ interface ShowMoreProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ShowMore = ({
|
||||
text = "Show more",
|
||||
onClick,
|
||||
className = "",
|
||||
}: ShowMoreProps) => {
|
||||
const ShowMore = ({ text, onClick, className = "" }: ShowMoreProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const defaultText = formatMessage(messages.ShowMore);
|
||||
const classNames = className ? `show-more ${className}` : "show-more";
|
||||
return (
|
||||
<div className="show-more-container">
|
||||
<button className={classNames} onClick={onClick}>
|
||||
{text}
|
||||
{text || defaultText}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -34,3 +34,9 @@
|
||||
.tabs > div {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import "./Tabs.css";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export interface Tab {
|
||||
text: string;
|
||||
text: ReactElement | string;
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
@ -18,8 +20,10 @@ interface TabElementProps extends Omit<TabsProps, "tabs"> {
|
||||
export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`tab ${tab.value === t.value ? "active" : ""}`}
|
||||
onClick={() => setTab(t)}
|
||||
className={`tab ${tab.value === t.value ? "active" : ""} ${
|
||||
t.disabled ? "disabled" : ""
|
||||
}`}
|
||||
onClick={() => !t.disabled && setTab(t)}
|
||||
>
|
||||
{t.text}
|
||||
</div>
|
||||
@ -29,17 +33,9 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => {
|
||||
const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||
return (
|
||||
<div className="tabs">
|
||||
{tabs.map((t) => {
|
||||
return (
|
||||
<div
|
||||
key={t.value}
|
||||
className={`tab ${tab.value === t.value ? "active" : ""}`}
|
||||
onClick={() => setTab(t)}
|
||||
>
|
||||
{t.text}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{tabs.map((t) => (
|
||||
<TabElement tab={tab} setTab={setTab} t={t} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||
import "./Textarea.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||
import emoji from "@jukben/emoji-search";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
@ -12,6 +13,8 @@ import { hexToBech32 } from "Util";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { useQuery } from "State/Users/Hooks";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface EmojiItemProps {
|
||||
name: string;
|
||||
char: string;
|
||||
@ -43,6 +46,7 @@ const UserItem = (metadata: MetadataCache) => {
|
||||
|
||||
const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const allUsers = useQuery(query);
|
||||
|
||||
@ -61,7 +65,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||
<ReactTextareaAutocomplete
|
||||
{...rest}
|
||||
loadingComponent={() => <span>Loading....</span>}
|
||||
placeholder="What's on your mind?"
|
||||
placeholder={formatMessage(messages.NotePlaceholder)}
|
||||
onChange={onChange}
|
||||
textAreaComponent={TextareaAutosize}
|
||||
trigger={{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "./Thread.css";
|
||||
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 { TaggedRawEvent, u256, HexKey } from "Nostr";
|
||||
@ -11,7 +11,8 @@ import BackButton from "Element/BackButton";
|
||||
import Note from "Element/Note";
|
||||
import NoteGhost from "Element/NoteGhost";
|
||||
import Collapsed from "Element/Collapsed";
|
||||
import type { RootState } from "State/Store";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
function getParent(
|
||||
ev: HexKey,
|
||||
@ -115,6 +116,7 @@ const ThreadNote = ({
|
||||
chains,
|
||||
onNavigate,
|
||||
}: ThreadNoteProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const replies = getReplies(note.Id, chains);
|
||||
const activeInReplies = replies.map((r) => r.Id).includes(active);
|
||||
const [collapsed, setCollapsed] = useState(!activeInReplies);
|
||||
@ -150,7 +152,7 @@ const ThreadNote = ({
|
||||
/>
|
||||
) : (
|
||||
<Collapsed
|
||||
text="Show replies"
|
||||
text={formatMessage(messages.ShowReplies)}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
@ -261,7 +263,7 @@ const TierThree = ({
|
||||
type="button"
|
||||
onClick={() => onNavigate(from)}
|
||||
>
|
||||
Show replies
|
||||
<FormattedMessage {...messages.ShowReplies} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import "./Timeline.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faForward } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useCallback, useMemo } from "react";
|
||||
@ -14,6 +15,8 @@ import useModeration from "Hooks/useModeration";
|
||||
import ProfilePreview from "./ProfilePreview";
|
||||
import Skeleton from "Element/Skeleton";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface TimelineProps {
|
||||
postsOnly: boolean;
|
||||
subject: TimelineSubject;
|
||||
@ -96,8 +99,11 @@ export default function Timeline({
|
||||
<div className="main-content">
|
||||
{latestFeed.length > 1 && (
|
||||
<div className="card latest-notes pointer" onClick={() => showLatest()}>
|
||||
<FontAwesomeIcon icon={faForward} size="xl" />
|
||||
Show latest {latestFeed.length - 1} notes
|
||||
<FontAwesomeIcon icon={faForward} size="xl" />{" "}
|
||||
<FormattedMessage
|
||||
{...messages.ShowLatest}
|
||||
values={{ n: latestFeed.length - 1 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mainFeed.map(eventElement)}
|
||||
|
@ -87,3 +87,7 @@
|
||||
.amount-number {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.zap.note .body {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "./Zap.css";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
// @ts-expect-error
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
@ -14,6 +15,8 @@ import Text from "Element/Text";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
function findTag(e: TaggedRawEvent, tag: string) {
|
||||
const maybeTag = e.tags.find((evTag) => {
|
||||
return evTag[0] === tag;
|
||||
@ -55,7 +58,7 @@ function getZapper(zap: TaggedRawEvent, dhash: string): Zapper {
|
||||
return { isValid: false };
|
||||
}
|
||||
|
||||
interface ParsedZap {
|
||||
export interface ParsedZap {
|
||||
id: HexKey;
|
||||
e?: HexKey;
|
||||
p: HexKey;
|
||||
@ -91,23 +94,30 @@ const Zap = ({
|
||||
const { amount, content, zapper, valid, p } = zap;
|
||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
|
||||
return valid ? (
|
||||
return valid && zapper ? (
|
||||
<div className="zap note card">
|
||||
<div className="header">
|
||||
{zapper ? <ProfileImage pubkey={zapper} /> : <div>Anon </div>}
|
||||
<ProfileImage pubkey={zapper} />
|
||||
{p !== pubKey && showZapped && <ProfileImage pubkey={p} />}
|
||||
<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 className="body">
|
||||
<Text
|
||||
creator={zapper || ""}
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
/>
|
||||
</div>
|
||||
{content.length > 0 && zapper && (
|
||||
<div className="body">
|
||||
<Text
|
||||
creator={zapper}
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@ -118,8 +128,8 @@ interface ZapsSummaryProps {
|
||||
|
||||
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
const sortedZaps = useMemo(() => {
|
||||
const pub = [...zaps.filter((z) => z.zapper)];
|
||||
const priv = [...zaps.filter((z) => !z.zapper)];
|
||||
const pub = [...zaps.filter((z) => z.zapper && z.valid)];
|
||||
const priv = [...zaps.filter((z) => !z.zapper && z.valid)];
|
||||
pub.sort((a, b) => b.amount - a.amount);
|
||||
return pub.concat(priv);
|
||||
}, [zaps]);
|
||||
@ -129,8 +139,7 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
}
|
||||
|
||||
const [topZap, ...restZaps] = sortedZaps;
|
||||
const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const { zapper, amount, content, valid } = topZap;
|
||||
const { zapper, amount } = topZap;
|
||||
|
||||
return (
|
||||
<div className="zaps-summary">
|
||||
@ -139,11 +148,15 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
<div className="summary">
|
||||
{zapper && <ProfileImage pubkey={zapper} />}
|
||||
{restZaps.length > 0 && (
|
||||
<span>
|
||||
and {restZaps.length} other{restZaps.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
<span> zapped</span>
|
||||
<FormattedMessage
|
||||
{...messages.Others}
|
||||
values={{ n: restZaps.length }}
|
||||
/>
|
||||
)}{" "}
|
||||
<FormattedMessage
|
||||
{...messages.OthersZapped}
|
||||
values={{ n: restZaps.length }}
|
||||
/>
|
||||
</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");
|
Reference in New Issue
Block a user