From 2b29fb08973a1f448ab526f7ed8d35546c0c5351 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 8 Feb 2023 22:10:26 +0100 Subject: [PATCH] feat: translations --- src/Element/BackButton.tsx | 8 +- src/Element/BlockButton.tsx | 7 +- src/Element/BlockList.tsx | 17 ++- src/Element/DM.tsx | 9 +- src/Element/FollowButton.tsx | 11 +- src/Element/FollowListBase.tsx | 6 +- src/Element/FollowersList.tsx | 12 +- src/Element/FollowsList.tsx | 12 +- src/Element/FollowsYou.tsx | 13 ++- src/Element/Invoice.tsx | 21 +++- src/Element/LoadMore.tsx | 5 +- src/Element/LogoutButton.tsx | 5 +- src/Element/MuteButton.tsx | 7 +- src/Element/MutedList.tsx | 16 ++- src/Element/Nip5Service.tsx | 65 +++++++---- src/Element/Note.tsx | 48 ++++++-- src/Element/NoteCreator.tsx | 13 ++- src/Element/NoteFooter.tsx | 104 ++++++++++++------ src/Element/NoteTime.tsx | 9 +- src/Element/NoteToSelf.tsx | 7 +- src/Element/Reactions.css | 118 ++++++++++++++++++++ src/Element/Reactions.tsx | 166 ++++++++++++++++++++++++++++ src/Element/Relay.tsx | 19 +++- src/Element/SendSats.css | 9 -- src/Element/SendSats.tsx | 67 ++++++++---- src/Element/ShowMore.tsx | 13 ++- src/Element/Tabs.css | 6 + src/Element/Tabs.tsx | 24 ++-- src/Element/Textarea.tsx | 6 +- src/Element/Thread.tsx | 10 +- src/Element/Timeline.tsx | 10 +- src/Element/Zap.css | 4 + src/Element/Zap.tsx | 55 ++++++---- src/Element/messages.js | 96 ++++++++++++++++ src/Feed/TimelineFeed.ts | 1 + src/Icons/Dislike.tsx | 5 +- src/Icons/Heart.tsx | 5 +- src/Pages/MessagesPage.tsx | 9 +- src/Pages/ProfilePage.tsx | 62 +++++++---- src/Pages/Root.tsx | 32 ++++-- src/Pages/SearchPage.tsx | 10 +- src/Pages/SettingsPage.tsx | 5 +- src/Pages/Verification.tsx | 41 ++++--- src/Pages/messages.js | 31 +++++- src/Pages/settings/Index.tsx | 15 ++- src/Pages/settings/Preferences.tsx | 106 ++++++++++++------ src/Pages/settings/Profile.tsx | 52 ++++++--- src/Pages/settings/RelayInfo.tsx | 23 +++- src/Pages/settings/Relays.tsx | 11 +- src/Pages/settings/messages.js | 60 ++++++++++ src/Util.ts | 17 +++ src/index.css | 1 + src/translations/en.json | 169 ++++++++++++++++++++++++++++- src/translations/es.json | 167 +++++++++++++++++++++++++++- 54 files changed, 1505 insertions(+), 315 deletions(-) create mode 100644 src/Element/Reactions.css create mode 100644 src/Element/Reactions.tsx create mode 100644 src/Element/messages.js create mode 100644 src/Pages/settings/messages.js diff --git a/src/Element/BackButton.tsx b/src/Element/BackButton.tsx index 61d060f7..e9252e15 100644 --- a/src/Element/BackButton.tsx +++ b/src/Element/BackButton.tsx @@ -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 ( ); }; diff --git a/src/Element/BlockButton.tsx b/src/Element/BlockButton.tsx index 5f89ef5d..d201dc4d 100644 --- a/src/Element/BlockButton.tsx +++ b/src/Element/BlockButton.tsx @@ -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) ? ( ) : ( ); }; diff --git a/src/Element/BlockList.tsx b/src/Element/BlockList.tsx index 7640d239..b408c7d3 100644 --- a/src/Element/BlockList.tsx +++ b/src/Element/BlockList.tsx @@ -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) {
{variant === "muted" && ( <> -

{muted.length} muted

+

+ +

{muted.map((a) => { return ( -

{blocked.length} blocked

+

+ +

{blocked.map((a) => { return (
- +
(isFollowing ? unfollow(pubkey) : follow(pubkey))} > - {isFollowing ? "Unfollow" : "Follow"} + {isFollowing ? ( + + ) : ( + + )} ); } diff --git a/src/Element/FollowListBase.tsx b/src/Element/FollowListBase.tsx index 57d94932..4479a2dc 100644 --- a/src/Element/FollowListBase.tsx +++ b/src/Element/FollowListBase.tsx @@ -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 +
{pubkeys?.map((a) => ( diff --git a/src/Element/FollowersList.tsx b/src/Element/FollowersList.tsx index 1d614e52..cc903511 100644 --- a/src/Element/FollowersList.tsx +++ b/src/Element/FollowersList.tsx @@ -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 ( - + ); } diff --git a/src/Element/FollowsList.tsx b/src/Element/FollowsList.tsx index e279488d..a6557d97 100644 --- a/src/Element/FollowsList.tsx +++ b/src/Element/FollowsList.tsx @@ -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 ( - + ); } diff --git a/src/Element/FollowsYou.tsx b/src/Element/FollowsYou.tsx index 503110fe..fdf32b16 100644 --- a/src/Element/FollowsYou.tsx +++ b/src/Element/FollowsYou.tsx @@ -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( (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 ? follows you : null} - ); + return followsMe ? ( + {formatMessage(messages.FollowsYou)} + ) : null; } diff --git a/src/Element/Invoice.tsx b/src/Element/Invoice.tsx index f06871f2..b960dd12 100644 --- a/src/Element/Invoice.tsx +++ b/src/Element/Invoice.tsx @@ -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 ( <> -

Lightning Invoice

+

+ +

setShowInvoice(false)} @@ -102,10 +107,16 @@ export default function Invoice(props: InvoiceProps) {
{description &&

{description}

} {isPaid ? ( -
Paid
+
+ +
) : ( )}
diff --git a/src/Element/LoadMore.tsx b/src/Element/LoadMore.tsx index 3bb6f56d..e5cbf2a7 100644 --- a/src/Element/LoadMore.tsx +++ b/src/Element/LoadMore.tsx @@ -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 (
- {children ?? "Loading..."} + {children ?? }
); } diff --git a/src/Element/LogoutButton.tsx b/src/Element/LogoutButton.tsx index 8eb5c827..f3947f7c 100644 --- a/src/Element/LogoutButton.tsx +++ b/src/Element/LogoutButton.tsx @@ -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 + ); } diff --git a/src/Element/MuteButton.tsx b/src/Element/MuteButton.tsx index 9f87651f..8bc55c91 100644 --- a/src/Element/MuteButton.tsx +++ b/src/Element/MuteButton.tsx @@ -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) ? ( ) : ( ); }; diff --git a/src/Element/MutedList.tsx b/src/Element/MutedList.tsx index 5bf6d222..4d51a2c8 100644 --- a/src/Element/MutedList.tsx +++ b/src/Element/MutedList.tsx @@ -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 (
-
{`${pubkeys?.length} muted`}
+
+ +
{pubkeys?.map((a) => { diff --git a/src/Element/Nip5Service.tsx b/src/Element/Nip5Service.tsx index 14dc9bc5..dd2c2a5e 100644 --- a/src/Element/Nip5Service.tsx +++ b/src/Element/Nip5Service.tsx @@ -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((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) {

{props.name}

{props.about}

- Find out more info about {props.name} at{" "} - - {props.link} - + + {props.link} + + ), + }} + />

{error && {error.error}} {!registerStatus && ( @@ -196,7 +207,10 @@ export default function Nip5Service(props: Nip05ServiceProps) { {availabilityResponse?.available && !registerStatus && (
- {availabilityResponse.quote?.price.toLocaleString()} sats +
{availabilityResponse.quote?.data.type}
@@ -208,14 +222,14 @@ export default function Nip5Service(props: Nip05ServiceProps) { disabled /> startBuy(handle, domain)}> - Buy Now +
)} {availabilityResponse?.available === false && !registerStatus && (
- Not available:{" "} + {" "} {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 && (
-

Order Paid!

+

+ +

- Your new NIP-05 handle is:{" "} + {" "} {handle}@{domain}

-

Account Support

+

+ +

- Please make sure to save the following password in order to manage - your handle in the future +

- Go to{" "} + {" "} - account page +

-

Activate Now

+

+ +

updateProfile(handle, domain)}> - Add to Profile +
)} diff --git a/src/Element/Note.tsx b/src/Element/Note.tsx index e825d236..a8ffe5d3 100644 --- a/src/Element/Note.tsx +++ b/src/Element/Note.tsx @@ -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) => { ) : (
-

This author has been muted

- +

+ +

+
); @@ -76,6 +83,7 @@ export default function Note(props: NoteProps) { const baseClassname = `note card ${props.className ? props.className : ""}`; const [translated, setTranslated] = useState(); 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 Deleted; + return ( + + + + ); } return ( maxMentions - ? ` & ${othersLength} other${othersLength > 1 ? "s" : ""}` + ? formatMessage(messages.Others, { n: othersLength }) : ""; return (
@@ -181,7 +193,12 @@ export default function Note(props: NoteProps) { if (ev.Kind !== EventKind.TextNote) { return ( <> -

Unknown event kind: {ev.Kind}

+

+ +

{JSON.stringify(ev.ToObject(), undefined, "  ")}
); @@ -192,13 +209,20 @@ export default function Note(props: NoteProps) { return ( <>

- Translated from {translated.fromLanguage}: +

{translated.text} ); } else if (translated) { - return

Translation failed

; + return ( +

+ +

+ ); } } @@ -206,19 +230,19 @@ export default function Note(props: NoteProps) { if (!inView) return null; return ( <> - {options.showHeader ? ( + {options.showHeader && (
- {options.showTime ? ( + {options.showTime && (
- ) : null} + )}
- ) : null} + )}
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 + )} {options.showFooter && ( diff --git a/src/Element/NoteCreator.tsx b/src/Element/NoteCreator.tsx index 028de11d..dab6991d 100644 --- a/src/Element/NoteCreator.tsx +++ b/src/Element/NoteCreator.tsx @@ -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) {
diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index 5516846d..3f10b91b 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -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( (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) {
- {formatShort(groupReactions[Reaction.Positive])} + {formatShort(positive.length)}
{repostIcon()} @@ -250,42 +271,53 @@ export default function NoteFooter(props: NoteFooterProps) { return ( <> {prefs.enableReactions && ( - react("-")}> - - {formatShort(groupReactions[Reaction.Negative])} -   Dislike + setShowReactions(true)}> + + )} share()}> - Share + copyId()}> - Copy ID + mute(ev.PubKey)}> - Mute + + {prefs.enableReactions && ( + react("-")}> + + + + )} block(ev.PubKey)}> - Block + translate()}> - Translate to {langNames.of(lang.split("-")[0])} + {prefs.showDebugMenus && ( copyEvent()}> - Copy Event JSON + )} {isMine && ( deleteEvent()}> - Delete + )} @@ -326,6 +358,14 @@ export default function NoteFooter(props: NoteFooterProps) { show={reply} setShow={setReply} /> + setTip(false)} diff --git a/src/Element/NoteTime.tsx b/src/Element/NoteTime.tsx index f3cd14f5..d494a09b 100644 --- a/src/Element/NoteTime.tsx +++ b/src/Element/NoteTime.tsx @@ -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", diff --git a/src/Element/NoteToSelf.tsx b/src/Element/NoteToSelf.tsx index dac5dd2d..dc1949d3 100644 --- a/src/Element/NoteToSelf.tsx +++ b/src/Element/NoteToSelf.tsx @@ -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 (
- Note to Self + {" "} + {user?.nip05 && }
); diff --git a/src/Element/Reactions.css b/src/Element/Reactions.css new file mode 100644 index 00000000..daf775f5 --- /dev/null +++ b/src/Element/Reactions.css @@ -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; + } +} diff --git a/src/Element/Reactions.tsx b/src/Element/Reactions.tsx new file mode 100644 index 00000000..44e87d94 --- /dev/null +++ b/src/Element/Reactions.tsx @@ -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 ? ( + +
+
+ +
+
+

+ +

+
+ +
+ {tab.value === 0 && + likes.map((ev) => { + return ( +
+
+ {ev.content === "+" ? ( + + ) : ( + ev.content + )} +
+ + +
+ ); + })} + {tab.value === 1 && + zaps.map((z) => { + return ( +
+
+ + {formatShort(z.amount)} +
+ {z.content}} + /> + +
+ ); + })} + {tab.value === 2 && + reposts.map((ev) => { + return ( +
+
+ +
+ + +
+ ); + })} + {tab.value === 3 && + dislikes.map((ev) => { + return ( +
+
+ +
+ + +
+ ); + })} +
+
+
+ ) : null; +}; + +export default Reactions; diff --git a/src/Element/Relay.tsx b/src/Element/Relay.tsx index 375f9f89..badb6bbb 100644 --- a/src/Element/Relay.tsx +++ b/src/Element/Relay.tsx @@ -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) {
{name}
- Write + @@ -71,7 +74,7 @@ export default function Relay(props: RelayProps) {
- Read + @@ -91,8 +94,12 @@ export default function Relay(props: RelayProps) {
{" "} {latency > 2000 - ? `${(latency / 1000).toFixed(0)} secs` - : `${latency.toLocaleString()} ms`} + ? formatMessage(messages.Seconds, { + n: (latency / 1000).toFixed(0), + }) + : formatMessage(messages.Milliseconds, { + n: latency.toLocaleString(), + })}   {state?.disconnects}
diff --git a/src/Element/SendSats.css b/src/Element/SendSats.css index 18b2a504..7141573d 100644 --- a/src/Element/SendSats.css +++ b/src/Element/SendSats.css @@ -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%; diff --git a/src/Element/SendSats.tsx b/src/Element/SendSats.tsx index dd77d89c..1d1f595b 100644 --- a/src/Element/SendSats.tsx +++ b/src/Element/SendSats.tsx @@ -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(); const [success, setSuccess] = useState(); 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 +
); @@ -220,7 +224,9 @@ export default function LNURLTip(props: LNURLTipProps) { if (invoice) return null; return ( <> -

Zap amount in sats

+

+ +

{serviceAmounts.map((a) => ( {payService && custom()}
- {(payService?.commentAllowed ?? 0) > 0 && ( - setComment(e.target.value)} - /> - )} + {(payService?.commentAllowed ?? 0) > 0 || + (payService?.nostrPubkey && ( + setComment(e.target.value)} + /> + ))}
{(amount ?? 0) > 0 && ( )} @@ -281,7 +297,7 @@ export default function LNURLTip(props: LNURLTipProps) { type="button" onClick={() => window.open(`lightning:${pr}`)} > - Open Wallet + )} @@ -297,7 +313,7 @@ export default function LNURLTip(props: LNURLTipProps) {

- {success?.description ?? "Paid!"} + {success?.description ?? }

{success.url && (

@@ -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 ( diff --git a/src/Element/ShowMore.tsx b/src/Element/ShowMore.tsx index 51027514..5d4bcf78 100644 --- a/src/Element/ShowMore.tsx +++ b/src/Element/ShowMore.tsx @@ -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 (

); diff --git a/src/Element/Tabs.css b/src/Element/Tabs.css index 6730f54d..539bad04 100644 --- a/src/Element/Tabs.css +++ b/src/Element/Tabs.css @@ -34,3 +34,9 @@ .tabs > div { cursor: pointer; } + +.tab.disabled { + opacity: 0.3; + cursor: not-allowed; + pointer-events: none; +} diff --git a/src/Element/Tabs.tsx b/src/Element/Tabs.tsx index 5f7e754e..4fff9f39 100644 --- a/src/Element/Tabs.tsx +++ b/src/Element/Tabs.tsx @@ -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 { export const TabElement = ({ t, tab, setTab }: TabElementProps) => { return (
setTab(t)} + className={`tab ${tab.value === t.value ? "active" : ""} ${ + t.disabled ? "disabled" : "" + }`} + onClick={() => !t.disabled && setTab(t)} > {t.text}
@@ -29,17 +33,9 @@ export const TabElement = ({ t, tab, setTab }: TabElementProps) => { const Tabs = ({ tabs, tab, setTab }: TabsProps) => { return (
- {tabs.map((t) => { - return ( -
setTab(t)} - > - {t.text} -
- ); - })} + {tabs.map((t) => ( + + ))}
); }; diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx index 4c352d2a..36463baa 100644 --- a/src/Element/Textarea.tsx +++ b/src/Element/Textarea.tsx @@ -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) => { Loading....} - placeholder="What's on your mind?" + placeholder={formatMessage(messages.NotePlaceholder)} onChange={onChange} textAreaComponent={TextareaAutosize} trigger={{ diff --git a/src/Element/Thread.tsx b/src/Element/Thread.tsx index eac0df0a..9c9f20dd 100644 --- a/src/Element/Thread.tsx +++ b/src/Element/Thread.tsx @@ -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 = ({ /> ) : ( @@ -261,7 +263,7 @@ const TierThree = ({ type="button" onClick={() => onNavigate(from)} > - Show replies +
) diff --git a/src/Element/Timeline.tsx b/src/Element/Timeline.tsx index 6d7d65fd..6317b30d 100644 --- a/src/Element/Timeline.tsx +++ b/src/Element/Timeline.tsx @@ -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({
{latestFeed.length > 1 && (
showLatest()}> - -   Show latest {latestFeed.length - 1} notes + {" "} +
)} {mainFeed.map(eventElement)} diff --git a/src/Element/Zap.css b/src/Element/Zap.css index a6bb03e3..2dd2c9e0 100644 --- a/src/Element/Zap.css +++ b/src/Element/Zap.css @@ -87,3 +87,7 @@ .amount-number { font-weight: bold; } + +.zap.note .body { + margin-bottom: 0; +} diff --git a/src/Element/Zap.tsx b/src/Element/Zap.tsx index 9766ebc7..e2ee5931 100644 --- a/src/Element/Zap.tsx +++ b/src/Element/Zap.tsx @@ -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 ? (
- {zapper ? :
Anon 
} + {p !== pubKey && showZapped && }
- {formatShort(amount)} sats + + +
-
- -
+ {content.length > 0 && zapper && ( +
+ +
+ )}
) : 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 (
@@ -139,11 +148,15 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
{zapper && } {restZaps.length > 0 && ( - - and {restZaps.length} other{restZaps.length > 1 ? "s" : ""} - - )} -  zapped + + )}{" "} +
)} diff --git a/src/Element/messages.js b/src/Element/messages.js new file mode 100644 index 00000000..500a000a --- /dev/null +++ b/src/Element/messages.js @@ -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"); diff --git a/src/Feed/TimelineFeed.ts b/src/Feed/TimelineFeed.ts index d017f215..ba7630e3 100644 --- a/src/Feed/TimelineFeed.ts +++ b/src/Feed/TimelineFeed.ts @@ -118,6 +118,7 @@ export default function useTimelineFeed( sub.Id = `timeline-related:${subject.type}`; sub.Kinds = new Set([ EventKind.Reaction, + EventKind.Repost, EventKind.Deletion, EventKind.ZapReceipt, ]); diff --git a/src/Icons/Dislike.tsx b/src/Icons/Dislike.tsx index e653210e..7059c3a9 100644 --- a/src/Icons/Dislike.tsx +++ b/src/Icons/Dislike.tsx @@ -1,4 +1,6 @@ -const Dislike = () => { +import IconProps from "./IconProps"; + +const Dislike = (props: IconProps) => { return ( { viewBox="0 0 19 20" fill="none" xmlns="http://www.w3.org/2000/svg" + {...props} > { +import IconProps from "./IconProps"; + +const Heart = (props: IconProps) => { return ( { viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg" + {...props} >
-

Messages

+

+ +

{chats diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index bb6c407f..3dd71805 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -1,6 +1,6 @@ import "./ProfilePage.css"; - import { useEffect, useMemo, useState } from "react"; +import { useIntl, FormattedMessage } from "react-intl"; import { useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; @@ -37,17 +37,18 @@ import Modal from "Element/Modal"; import { ProxyImg } from "Element/ProxyImg"; import useHorizontalScroll from "Hooks/useHorizontalScroll"; -const ProfileTab = { - Notes: { text: "Notes", value: 0 }, - Reactions: { text: "Reactions", value: 1 }, - Followers: { text: "Followers", value: 2 }, - Follows: { text: "Follows", value: 3 }, - Zaps: { text: "Zaps", value: 4 }, - Muted: { text: "Muted", value: 5 }, - Blocked: { text: "Blocked", value: 6 }, -}; +import messages from "./messages"; + +const NOTES = 0; +const REACTIONS = 1; +const FOLLOWERS = 2; +const FOLLOWS = 3; +const ZAPS = 4; +const MUTED = 5; +const BLOCKED = 6; export default function ProfilePage() { + const { formatMessage } = useIntl(); const params = useParams(); const navigate = useNavigate(); const id = useMemo(() => parseId(params.id!), [params]); @@ -61,7 +62,6 @@ export default function ProfilePage() { const follows = useSelector((s) => s.login.follows); const isMe = loginPubKey === id; const [showLnQr, setShowLnQr] = useState(false); - const [tab, setTab] = useState(ProfileTab.Notes); const [showProfileQr, setShowProfileQr] = useState(false); const aboutText = user?.about || ""; const about = Text({ @@ -85,6 +85,16 @@ export default function ProfilePage() { }, [zapFeed.store, id]); const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0); 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(ProfileTab.Notes); useEffect(() => { setTab(ProfileTab.Notes); @@ -149,8 +159,8 @@ export default function ProfilePage() { } function tabContent() { - switch (tab) { - case ProfileTab.Notes: + switch (tab.value) { + case NOTES: return ( ); - case ProfileTab.Zaps: { + case ZAPS: { return (
-

{formatShort(zapsTotal)} sats

+

+ +

{zaps.map((z) => ( ))} @@ -175,11 +190,16 @@ export default function ProfilePage() { ); } - case ProfileTab.Follows: { + case FOLLOWS: { if (isMe) { return (
-

Following {follows.length}

+

+ +

{follows.map((a) => ( ; } } - case ProfileTab.Followers: { + case FOLLOWERS: { return ; } - case ProfileTab.Muted: { + case MUTED: { return isMe ? : ; } - case ProfileTab.Blocked: { + case BLOCKED: { return isMe ? : null; } } @@ -233,7 +253,7 @@ export default function ProfilePage() { <> ) : ( diff --git a/src/Pages/Root.tsx b/src/Pages/Root.tsx index f8a1f230..9660d9ed 100644 --- a/src/Pages/Root.tsx +++ b/src/Pages/Root.tsx @@ -2,6 +2,7 @@ import "./Root.css"; import { useState } from "react"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; +import { FormattedMessage } from "react-intl"; import Tabs, { Tab } from "Element/Tabs"; import { RootState } from "State/Store"; @@ -9,10 +10,21 @@ import Timeline from "Element/Timeline"; import { HexKey } from "Nostr"; import { TimelineSubject } from "Feed/TimelineFeed"; +import messages from "./messages"; + const RootTab: Record = { - Posts: { text: "Posts", value: 0 }, - PostsAndReplies: { text: "Conversations", value: 1 }, - Global: { text: "Global", value: 2 }, + Posts: { + text: , + value: 0, + }, + PostsAndReplies: { + text: , + value: 1, + }, + Global: { + text: , + value: 2, + }, }; export default function RootPage() { @@ -25,10 +37,16 @@ export default function RootPage() { function followHints() { if (follows?.length === 0 && pubKey && tab !== RootTab.Global) { return ( - <> - Hmm nothing here.. Checkout New users page to - follow some recommended nostrich's! - + + + + ), + }} + /> ); } } diff --git a/src/Pages/SearchPage.tsx b/src/Pages/SearchPage.tsx index a6db22b4..bfa3330d 100644 --- a/src/Pages/SearchPage.tsx +++ b/src/Pages/SearchPage.tsx @@ -1,3 +1,4 @@ +import { useIntl, FormattedMessage } from "react-intl"; import { useParams } from "react-router-dom"; import Timeline from "Element/Timeline"; import { useEffect, useState } from "react"; @@ -6,8 +7,11 @@ import { router } from "index"; import { SearchRelays } from "Const"; import { System } from "Nostr/System"; +import messages from "./messages"; + const SearchPage = () => { const params: any = useParams(); + const { formatMessage } = useIntl(); const [search, setSearch] = useState(); const [keyword, setKeyword] = useState(params.keyword); @@ -39,12 +43,14 @@ const SearchPage = () => { return (
-

Search

+

+ +

setSearch(e.target.value)} /> diff --git a/src/Pages/SettingsPage.tsx b/src/Pages/SettingsPage.tsx index 2c999529..50207973 100644 --- a/src/Pages/SettingsPage.tsx +++ b/src/Pages/SettingsPage.tsx @@ -1,3 +1,4 @@ +import { FormattedMessage } from "react-intl"; import { Outlet, RouteObject, useNavigate } from "react-router-dom"; import SettingsIndex from "Pages/settings/Index"; import Profile from "Pages/settings/Profile"; @@ -5,13 +6,15 @@ import Relay from "Pages/settings/Relays"; import Preferences from "Pages/settings/Preferences"; import RelayInfo from "Pages/settings/RelayInfo"; +import messages from "./messages"; + export default function SettingsPage() { const navigate = useNavigate(); return (

navigate("/settings")} className="pointer"> - Settings +

diff --git a/src/Pages/Verification.tsx b/src/Pages/Verification.tsx index 3ab5192d..02e7154b 100644 --- a/src/Pages/Verification.tsx +++ b/src/Pages/Verification.tsx @@ -1,6 +1,10 @@ +import { FormattedMessage } from "react-intl"; + import { ApiHost } from "Const"; import Nip5Service from "Element/Nip5Service"; +import messages from "./messages"; + import "./Verification.css"; export default function VerificationPage() { @@ -10,42 +14,37 @@ export default function VerificationPage() { service: `${ApiHost}/api/v1/n5sp`, link: "https://snort.social/", supportLink: "https://snort.social/help", - about: ( - <> - Our very own NIP-05 verification service, help support the development - of this site and get a shiny special badge on our site! - - ), + about: , }, { name: "Nostr Plebs", service: "https://nostrplebs.com/api/v1", link: "https://nostrplebs.com/", supportLink: "https://nostrplebs.com/manage", - about: ( - <> -

- Nostr Plebs is one of the first NIP-05 providers in the space and - offers a good collection of domains at reasonable prices -

- - ), + about: , }, ]; return (
-

Get Verified

+

+ +

- NIP-05 is a DNS based verification spec which helps to validate you as a - real user. + +

+

+

-

Getting NIP-05 verified can help:

    -
  • Prevent fake accounts from imitating you
  • -
  • Make your profile easier to find and share
  • - Fund developers and platforms providing NIP-05 verification services + +
  • +
  • + +
  • +
  • +
diff --git a/src/Pages/messages.js b/src/Pages/messages.js index 5904e454..3178112e 100644 --- a/src/Pages/messages.js +++ b/src/Pages/messages.js @@ -3,6 +3,35 @@ import { addIdAndDefaultMessageToMessages } from "Util"; const messages = defineMessages({ 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"); diff --git a/src/Pages/settings/Index.tsx b/src/Pages/settings/Index.tsx index 96d44690..6683dabf 100644 --- a/src/Pages/settings/Index.tsx +++ b/src/Pages/settings/Index.tsx @@ -1,5 +1,5 @@ import "./Index.css"; - +import { FormattedMessage } from "react-intl"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; import ArrowFront from "Icons/ArrowFront"; @@ -8,9 +8,10 @@ import Profile from "Icons/Profile"; import Relay from "Icons/Relay"; import Heart from "Icons/Heart"; import Logout from "Icons/Logout"; - import { logout } from "State/Login"; +import messages from "./messages"; + const SettingsIndex = () => { const dispatch = useDispatch(); const navigate = useNavigate(); @@ -27,7 +28,9 @@ const SettingsIndex = () => {
- Profile + + +
@@ -36,7 +39,7 @@ const SettingsIndex = () => {
- Relays +
@@ -45,7 +48,7 @@ const SettingsIndex = () => {
- Preferences +
@@ -63,7 +66,7 @@ const SettingsIndex = () => {
- Log Out +
diff --git a/src/Pages/settings/Preferences.tsx b/src/Pages/settings/Preferences.tsx index 03eba57b..fef772a3 100644 --- a/src/Pages/settings/Preferences.tsx +++ b/src/Pages/settings/Preferences.tsx @@ -1,7 +1,12 @@ +import "./Preferences.css"; + import { useDispatch, useSelector } from "react-redux"; +import { FormattedMessage } from "react-intl"; + import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login"; import { RootState } from "State/Store"; -import "./Preferences.css"; + +import messages from "./messages"; const PreferencesPage = () => { const dispatch = useDispatch(); @@ -11,11 +16,15 @@ const PreferencesPage = () => { return (
-

Preferences

+

+ +

-
Theme
+
+ +
-
Automatically load media
+
+ +
- Media in posts will automatically be shown for selected people, - otherwise only the link will show +
@@ -55,17 +71,27 @@ const PreferencesPage = () => { ) } > - - - + + +
-
Image proxy service
- Use imgproxy to compress images +
+ +
+ + +
{ {perf.imgProxyConfig && (
-
Service Url
+
+ +
dispatch( setPreferences({ @@ -106,7 +134,9 @@ const PreferencesPage = () => {
-
Service Key
+
+ +
{
-
Service Salt
+
+ +
{
-
Enable reactions
+
+ +
- Reactions will be shown on every page, if disabled no reactions will - be shown +
@@ -172,8 +205,12 @@ const PreferencesPage = () => {
-
Confirm reposts
- Reposts need to be manually confirmed +
+ +
+ + +
{
-
Automatically show latest notes
+
+ +
- Notes will stream in real time into global and posts tab +
@@ -208,9 +247,11 @@ const PreferencesPage = () => {
-
File upload service
+
+ +
- Pick which upload service you want to upload attachments to +
@@ -225,7 +266,9 @@ const PreferencesPage = () => { ) } > - + @@ -233,10 +276,11 @@ const PreferencesPage = () => {
-
Debug Menus
+
+ +
- Shows "Copy ID" and "Copy Event JSON" in the context menu on each - message +
diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index e69079a5..0be22333 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -1,7 +1,7 @@ import "./Profile.css"; import Nostrich from "nostrich.webp"; - import { useEffect, useState } from "react"; +import { FormattedMessage } from "react-intl"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -15,6 +15,8 @@ import { RootState } from "State/Store"; import { HexKey } from "Nostr"; import useFileUpload from "Upload"; +import messages from "./messages"; + export interface ProfileSettingsProps { avatar?: boolean; banner?: boolean; @@ -112,7 +114,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) { return (
-
Name:
+
+ : +
-
Display name:
+
+ : +
-
About:
+
+ : +