feat: migrate note component to i18n

This commit is contained in:
reya 2024-01-29 10:22:55 +07:00
parent b97676dd3e
commit 698bd78684
18 changed files with 125 additions and 47 deletions

View File

@ -32,6 +32,7 @@
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",
"react": "^18.2.0", "react": "^18.2.0",
"react-currency-input-field": "^3.6.14", "react-currency-input-field": "^3.6.14",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"sonner": "^1.3.1", "sonner": "^1.3.1",

View File

@ -1,11 +1,14 @@
import { PinIcon } from "@lume/icons"; import { PinIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils"; import { COL_TYPES } from "@lume/utils";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useColumnContext } from "../../column/provider"; import { useColumnContext } from "../../column/provider";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
export function NotePin() { export function NotePin() {
const event = useNoteContext(); const event = useNoteContext();
const { t } = useTranslation();
const { addColumn } = useColumnContext(); const { addColumn } = useColumnContext();
return ( return (
@ -24,12 +27,12 @@ export function NotePin() {
className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900" className="inline-flex items-center justify-center gap-2 pl-2 pr-3 text-sm font-medium rounded-full h-7 w-max bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:bg-neutral-900"
> >
<PinIcon className="size-4" /> <PinIcon className="size-4" />
Pin {t("note.buttons.pin")}
</button> </button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Pin note {t("note.buttons.pinTooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>

View File

@ -1,5 +1,6 @@
import { ReplyIcon } from "@lume/icons"; import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
@ -7,6 +8,8 @@ export function NoteReply() {
const event = useNoteContext(); const event = useNoteContext();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
return ( return (
<Tooltip.Provider> <Tooltip.Provider>
<Tooltip.Root delayDuration={150}> <Tooltip.Root delayDuration={150}>
@ -21,7 +24,7 @@ export function NoteReply() {
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
View thread {t("note.menu.viewThread")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>

View File

@ -5,6 +5,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
@ -13,6 +14,7 @@ export function NoteRepost() {
const setEditorValue = useSetAtom(editorValueAtom); const setEditorValue = useSetAtom(editorValueAtom);
const setIsEditorOpen = useSetAtom(editorAtom); const setIsEditorOpen = useSetAtom(editorAtom);
const [t] = useTranslation();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false); const [isRepost, setIsRepost] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -81,7 +83,7 @@ export function NoteRepost() {
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Repost {t("note.buttons.repost")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>
@ -96,7 +98,7 @@ export function NoteRepost() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<RepostIcon className="size-4" /> <RepostIcon className="size-4" />
Repost {t("note.buttons.repost")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -106,7 +108,7 @@ export function NoteRepost() {
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<ReplyIcon className="size-4" /> <ReplyIcon className="size-4" />
Quote {t("note.buttons.quote")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>

View File

@ -8,6 +8,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
import { useState } from "react"; import { useState } from "react";
import CurrencyInput from "react-currency-input-field"; import CurrencyInput from "react-currency-input-field";
import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { useProfile } from "../../../hooks/useProfile"; import { useProfile } from "../../../hooks/useProfile";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
@ -23,6 +24,7 @@ export function NoteZap() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [invoice, setInvoice] = useState<string>(null); const [invoice, setInvoice] = useState<string>(null);
const { t } = useTranslation();
const { user } = useProfile(event.pubkey); const { user } = useProfile(event.pubkey);
const createZapRequest = async (instant?: boolean) => { const createZapRequest = async (instant?: boolean) => {
@ -99,7 +101,7 @@ export function NoteZap() {
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Zap {t("note.zap.tooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>
@ -124,7 +126,7 @@ export function NoteZap() {
</Dialog.Trigger> </Dialog.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Zap {t("note.zap.tooltip")}
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>
@ -145,7 +147,7 @@ export function NoteZap() {
<div className="inline-flex items-center justify-center w-full px-5 py-3 shrink-0"> <div className="inline-flex items-center justify-center w-full px-5 py-3 shrink-0">
<div className="w-6" /> <div className="w-6" />
<Dialog.Title className="font-semibold text-center"> <Dialog.Title className="font-semibold text-center">
Send zap to{" "} {t("note.zap.modalTitle")}{" "}
{user?.name || {user?.name ||
user?.displayName || user?.displayName ||
displayNpub(event.pubkey, 16)} displayNpub(event.pubkey, 16)}
@ -217,7 +219,7 @@ export function NoteZap() {
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
placeholder="Enter message (optional)" placeholder={t("note.zap.messagePlaceholder")}
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400" className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -227,10 +229,10 @@ export function NoteZap() {
className="inline-flex items-center justify-center w-full pb-[2px] font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex items-center justify-center w-full pb-[2px] font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
> >
{isCompleted {isCompleted
? "Zapped" ? t("note.zap.buttonFinish")
: isLoading : isLoading
? "Processing..." ? t("note.zap.buttonLoading")
: "Zap"} : t("note.zap.zap")}
</button> </button>
</div> </div>
</div> </div>
@ -241,11 +243,11 @@ export function NoteZap() {
<QRCodeSVG value={invoice} size={256} /> <QRCodeSVG value={invoice} size={256} />
</div> </div>
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<h3 className="text-lg font-medium">Scan to zap</h3> <h3 className="text-lg font-medium">
{t("note.zap.invoiceButton")}
</h3>
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400"> <span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
You must use Bitcoin wallet which support Lightning {t("note.zap.invoiceFooter")}
<br />
such as: Blue Wallet, Bitkit, Phoenix,...
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { NOSTR_MENTIONS } from "@lume/utils"; import { NOSTR_MENTIONS } from "@lume/utils";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { nip19 } from "nostr-tools";
import { ReactNode, useMemo } from "react"; import { ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { useEvent } from "../../hooks/useEvent"; import { useEvent } from "../../hooks/useEvent";
@ -13,6 +13,7 @@ export function NoteChild({
eventId, eventId,
isRoot, isRoot,
}: { eventId: string; isRoot?: boolean }) { }: { eventId: string; isRoot?: boolean }) {
const { t } = useTranslation();
const { isLoading, isError, data } = useEvent(eventId); const { isLoading, isError, data } = useEvent(eventId);
const richContent = useMemo(() => { const richContent = useMemo(() => {
@ -91,7 +92,7 @@ export function NoteChild({
return ( return (
<div className="relative flex gap-3"> <div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800"> <div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
Failed to fetch event {t("note.error")}
</div> </div>
</div> </div>
); );
@ -111,7 +112,7 @@ export function NoteChild({
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight"> <div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
<User.Name className="max-w-[10rem] truncate" /> <User.Name className="max-w-[10rem] truncate" />
<div className="font-normal text-neutral-700 dark:text-neutral-300"> <div className="font-normal text-neutral-700 dark:text-neutral-300">
{isRoot ? "posted:" : "replied:"} {isRoot ? t("note.posted") : t("note.replied")}:
</div> </div>
</div> </div>
</User.Root> </User.Root>

View File

@ -1,6 +1,7 @@
import { PinIcon } from "@lume/icons"; import { PinIcon } from "@lume/icons";
import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils"; import { COL_TYPES, NOSTR_MENTIONS } from "@lume/utils";
import { ReactNode, useMemo } from "react"; import { ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { useEvent } from "../../../hooks/useEvent"; import { useEvent } from "../../../hooks/useEvent";
@ -13,6 +14,7 @@ export function MentionNote({
eventId, eventId,
openable = true, openable = true,
}: { eventId: string; openable?: boolean }) { }: { eventId: string; openable?: boolean }) {
const { t } = useTranslation();
const { addColumn } = useColumnContext(); const { addColumn } = useColumnContext();
const { isLoading, isError, data } = useEvent(eventId); const { isLoading, isError, data } = useEvent(eventId);
@ -98,7 +100,7 @@ export function MentionNote({
contentEditable={false} contentEditable={false}
className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900" className="w-full p-3 my-1 rounded-lg cursor-default bg-neutral-100 dark:bg-neutral-900"
> >
Failed to fetch event. {t("note.error")}
</div> </div>
); );
} }
@ -127,7 +129,7 @@ export function MentionNote({
to={`/events/${data.id}`} to={`/events/${data.id}`}
className="text-sm text-blue-500 hover:text-blue-600" className="text-sm text-blue-500 hover:text-blue-600"
> >
Show more {t("note.showMore")}
</Link> </Link>
<button <button
type="button" type="button"

View File

@ -1,5 +1,6 @@
import { COL_TYPES } from "@lume/utils"; import { COL_TYPES } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useArk } from "../../../hooks/useArk"; import { useArk } from "../../../hooks/useArk";
import { useProfile } from "../../../hooks/useProfile"; import { useProfile } from "../../../hooks/useProfile";
@ -10,6 +11,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
const cleanPubkey = ark.getCleanPubkey(pubkey); const cleanPubkey = ark.getCleanPubkey(pubkey);
const { isLoading, isError, user } = useProfile(pubkey); const { isLoading, isError, user } = useProfile(pubkey);
const { t } = useTranslation();
const { addColumn } = useColumnContext(); const { addColumn } = useColumnContext();
return ( return (
@ -27,7 +29,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
to={`/users/${cleanPubkey}`} to={`/users/${cleanPubkey}`}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
View profile {t("note.buttons.viewProfile")}
</Link> </Link>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -36,13 +38,13 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
onClick={async () => onClick={async () =>
await addColumn({ await addColumn({
kind: COL_TYPES.user, kind: COL_TYPES.user,
title: user?.name || user?.displayName || "Profile", title: user?.name || user?.displayName || "User",
content: cleanPubkey, content: cleanPubkey,
}) })
} }
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Pin {t("note.buttons.pin")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>

View File

@ -5,6 +5,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { type EventPointer } from "nostr-tools/lib/types/nip19"; import { type EventPointer } from "nostr-tools/lib/types/nip19";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { useColumnContext } from "../column/provider"; import { useColumnContext } from "../column/provider";
@ -13,7 +14,10 @@ import { useNoteContext } from "./provider";
export function NoteMenu() { export function NoteMenu() {
const event = useNoteContext(); const event = useNoteContext();
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation();
const { addColumn } = useColumnContext(); const { addColumn } = useColumnContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const copyID = async () => { const copyID = async () => {
@ -67,7 +71,7 @@ export function NoteMenu() {
onClick={() => copyLink()} onClick={() => copyLink()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
View thread {t("note.menu.viewThread")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -76,7 +80,7 @@ export function NoteMenu() {
onClick={() => navigate(`/events/${event.id}`)} onClick={() => navigate(`/events/${event.id}`)}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy shareable link {t("note.menu.copyLink")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -85,7 +89,7 @@ export function NoteMenu() {
onClick={() => copyID()} onClick={() => copyID()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy note ID {t("note.menu.copyNoteId")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -94,7 +98,7 @@ export function NoteMenu() {
onClick={() => copyNpub()} onClick={() => copyNpub()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy author ID {t("note.menu.copyAuthorId")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -102,7 +106,7 @@ export function NoteMenu() {
to={`/users/${event.pubkey}`} to={`/users/${event.pubkey}`}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
View author {t("note.menu.viewAuthor")}
</Link> </Link>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -117,7 +121,7 @@ export function NoteMenu() {
} }
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Pin author {t("note.menu.pinAuthor")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" /> <DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
@ -127,7 +131,7 @@ export function NoteMenu() {
onClick={() => copyRaw()} onClick={() => copyRaw()}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy raw event {t("note.menu.copyRaw")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
@ -136,7 +140,7 @@ export function NoteMenu() {
onClick={muteUser} onClick={muteUser}
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none" className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
> >
Mute {t("note.menu.mute")}
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>

View File

@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useArk } from "../../hooks/useArk"; import { useArk } from "../../hooks/useArk";
import { AppHandler } from "./appHandler"; import { AppHandler } from "./appHandler";
import { useNoteContext } from "./provider"; import { useNoteContext } from "./provider";
@ -7,6 +8,7 @@ export function NIP89({ className }: { className?: string }) {
const ark = useArk(); const ark = useArk();
const event = useNoteContext(); const event = useNoteContext();
const { t } = useTranslation();
const { isLoading, isError, data } = useQuery({ const { isLoading, isError, data } = useQuery({
queryKey: ["app-recommend", event.id], queryKey: ["app-recommend", event.id],
queryFn: () => { queryFn: () => {
@ -33,7 +35,7 @@ export function NIP89({ className }: { className?: string }) {
<div className="flex flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900"> <div className="flex flex-col rounded-lg bg-neutral-100 dark:bg-neutral-900">
<div className="inline-flex items-center justify-between h-10 px-3 border-b shrink-0 border-neutral-200 dark:border-neutral-800"> <div className="inline-flex items-center justify-between h-10 px-3 border-b shrink-0 border-neutral-200 dark:border-neutral-800">
<p className="text-sm font-medium text-amber-400"> <p className="text-sm font-medium text-amber-400">
Lume isn't support this event {t("nip89.unsupported")}
</p> </p>
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <p className="text-sm text-neutral-600 dark:text-neutral-400">
{event.kind} {event.kind}
@ -41,10 +43,10 @@ export function NIP89({ className }: { className?: string }) {
</div> </div>
<div className="flex flex-col flex-1 gap-2 px-3 py-3"> <div className="flex flex-col flex-1 gap-2 px-3 py-3">
<span className="text-sm font-medium uppercase text-neutral-600 dark:text-neutral-400"> <span className="text-sm font-medium uppercase text-neutral-600 dark:text-neutral-400">
Open with {t("nip89.openWith")}
</span> </span>
{data.map((item, index) => ( {data.map((item) => (
<AppHandler key={item[1] + index} tag={item} /> <AppHandler key={item[1]} tag={item} />
))} ))}
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import { NDKEventWithReplies } from "@lume/types";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Note } from ".."; import { Note } from "..";
import { ChildReply } from "./childReply"; import { ChildReply } from "./childReply";
@ -11,6 +12,7 @@ export function Reply({
}: { }: {
event: NDKEventWithReplies; event: NDKEventWithReplies;
}) { }) {
const [t] = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@ -30,7 +32,9 @@ export function Reply({
className={cn("size-5", open ? "rotate-180 transform" : "")} className={cn("size-5", open ? "rotate-180 transform" : "")}
/> />
{`${event.replies?.length} ${ {`${event.replies?.length} ${
event.replies?.length === 1 ? "reply" : "replies" event.replies?.length === 1
? t("note.reply.single")
: t("note.reply.plural")
}`} }`}
</div> </div>
</Collapsible.Trigger> </Collapsible.Trigger>

View File

@ -2,6 +2,7 @@ import { RepostIcon } from "@lume/icons";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk"; import { NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { Note } from ".."; import { Note } from "..";
import { useArk } from "../../../hooks/useArk"; import { useArk } from "../../../hooks/useArk";
import { User } from "../../user"; import { User } from "../../user";
@ -12,6 +13,7 @@ export function RepostNote({
}: { event: NDKEvent; className?: string }) { }: { event: NDKEvent; className?: string }) {
const ark = useArk(); const ark = useArk();
const { t } = useTranslation();
const { const {
isLoading, isLoading,
isError, isError,
@ -51,7 +53,7 @@ export function RepostNote({
<User.Avatar className="size-6 shrink-0 rounded object-cover" /> <User.Avatar className="size-6 shrink-0 rounded object-cover" />
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" /> <User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">reposted</span> <span className="text-blue-500">{t("note.reposted")}</span>
</div> </div>
</div> </div>
</User.Root> </User.Root>
@ -59,10 +61,6 @@ export function RepostNote({
<div className="px-3 mb-3 select-text"> <div className="px-3 mb-3 select-text">
<div className="flex flex-col items-start justify-start px-3 py-3 bg-red-100 rounded-lg dark:bg-red-900"> <div className="flex flex-col items-start justify-start px-3 py-3 bg-red-100 rounded-lg dark:bg-red-900">
<p className="text-red-500">Failed to get event</p> <p className="text-red-500">Failed to get event</p>
<p className="text-sm">
You can consider enable Outbox in Settings for better event
discovery.
</p>
</div> </div>
</div> </div>
</Note.Root> </Note.Root>
@ -85,7 +83,7 @@ export function RepostNote({
<User.Avatar className="size-6 shrink-0 rounded object-cover" /> <User.Avatar className="size-6 shrink-0 rounded object-cover" />
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" /> <User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">reposted</span> <span className="text-blue-500">{t("note.reposted")}</span>
</div> </div>
</div> </div>
</User.Root> </User.Root>

View File

@ -1,5 +1,6 @@
import { PinIcon } from "@lume/icons"; import { PinIcon } from "@lume/icons";
import { COL_TYPES, cn } from "@lume/utils"; import { COL_TYPES, cn } from "@lume/utils";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Note } from "."; import { Note } from ".";
import { useArk } from "../../hooks/useArk"; import { useArk } from "../../hooks/useArk";
@ -18,6 +19,7 @@ export function NoteThread({
tags: event.tags, tags: event.tags,
}); });
const { t } = useTranslation();
const { addColumn } = useColumnContext(); const { addColumn } = useColumnContext();
if (!thread) return null; if (!thread) return null;
@ -36,7 +38,7 @@ export function NoteThread({
to={`/events/${thread?.rootEventId || thread?.replyEventId}`} to={`/events/${thread?.rootEventId || thread?.replyEventId}`}
className="self-start text-blue-500 hover:text-blue-600" className="self-start text-blue-500 hover:text-blue-600"
> >
Show thread {t("note.showThread")}
</Link> </Link>
<button <button
type="button" type="button"

View File

@ -1,4 +1,4 @@
import { UnverifiedIcon, VerifiedIcon } from "@lume/icons"; import { VerifiedIcon } from "@lume/icons";
import { cn, displayNpub } from "@lume/utils"; import { cn, displayNpub } from "@lume/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useArk } from "../../hooks/useArk"; import { useArk } from "../../hooks/useArk";

View File

@ -1,3 +1,4 @@
import { NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useArk } from "./useArk"; import { useArk } from "./useArk";
@ -20,7 +21,7 @@ export function useProfile(pubkey: string) {
return profile; return profile;
}, },
initialData: () => { initialData: () => {
return queryClient.getQueryData(["user", pubkey]); return queryClient.getQueryData(["user", pubkey]) as NDKUserProfile;
}, },
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,

View File

@ -26,6 +26,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"react-hotkeys-hook": "^4.4.4", "react-hotkeys-hook": "^4.4.4",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"slate": "^0.101.5", "slate": "^0.101.5",
"slate-react": "^0.101.6", "slate-react": "^0.101.6",

View File

@ -365,6 +365,9 @@ importers:
react-currency-input-field: react-currency-input-field:
specifier: ^3.6.14 specifier: ^3.6.14
version: 3.6.14(react@18.2.0) version: 3.6.14(react@18.2.0)
react-i18next:
specifier: ^14.0.1
version: 14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0)
react-router-dom: react-router-dom:
specifier: ^6.21.3 specifier: ^6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0) version: 6.21.3(react-dom@18.2.0)(react@18.2.0)
@ -999,6 +1002,9 @@ importers:
react-hotkeys-hook: react-hotkeys-hook:
specifier: ^4.4.4 specifier: ^4.4.4
version: 4.4.4(react-dom@18.2.0)(react@18.2.0) version: 4.4.4(react-dom@18.2.0)(react@18.2.0)
react-i18next:
specifier: ^14.0.1
version: 14.0.1(i18next@23.8.0)(react-dom@18.2.0)(react@18.2.0)
react-router-dom: react-router-dom:
specifier: ^6.21.3 specifier: ^6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0) version: 6.21.3(react-dom@18.2.0)(react@18.2.0)

View File

@ -3,11 +3,55 @@
"relay": "Relay", "relay": "Relay",
"continue": "Continue", "continue": "Continue",
"loading": "Loading", "loading": "Loading",
"error": "Error",
"moveLeft": "Move Left", "moveLeft": "Move Left",
"moveRight": "Move Right", "moveRight": "Move Right",
"newColumn": "New Column", "newColumn": "New Column",
"inspect": "Inspect" "inspect": "Inspect"
}, },
"nip89": {
"unsupported": "Lume isn't support this event",
"openWith": "Open with"
},
"note": {
"showThread": "Show thread",
"showMore": "Show more",
"error": "Failed to fetch event.",
"posted": "posted",
"replied": "replied",
"reposted": "reposted",
"menu": {
"viewThread": "View thread",
"copyLink": "Copy shareable link",
"copyNoteId": "Copy note ID",
"copyAuthorId": "Copy author ID",
"viewAuthor": "View author",
"pinAuthor": "Pin author",
"copyRaw": "Copy raw event",
"mute": "Mute"
},
"buttons": {
"pin": "Pin",
"pinTooltip": "Pin Note",
"repost": "Repost",
"quote": "Quote",
"viewProfile": "View profile"
},
"zap": {
"zap": "Zap",
"tooltip": "Send zap",
"modalTitle": "Send zap to",
"messagePlaceholder": "Enter message (optional)",
"buttonFinish": "Zapped",
"buttonLoading": "Processing...",
"invoiceButton": "Scan to zap",
"invoiceFooter": "You must use Bitcoin wallet which support Lightning\nsuch as: Blue Wallet, Bitkit, Phoenix,..."
},
"reply": {
"single": "reply",
"plural": "replies"
}
},
"welcome": { "welcome": {
"title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.", "title": "Lume is a magnificent client for Nostr to meet, explore\nand freely share your thoughts with everyone.",
"signup": "Join Nostr", "signup": "Join Nostr",