mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: polish
This commit is contained in:
parent
ccab78ca11
commit
e928f2ee37
@ -61,13 +61,17 @@ function Inactive({ pubkey }: { pubkey: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Active({ pubkey }: { pubkey: string }) {
|
function Active({ pubkey }: { pubkey: string }) {
|
||||||
const [open, setOpen] = useState(true);
|
const ark = useArk();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// @ts-ignore, magic !!!
|
// @ts-ignore, magic !!!
|
||||||
const { guest } = useSearch({ strict: false });
|
const { guest } = useSearch({ strict: false });
|
||||||
|
// @ts-ignore, magic !!!
|
||||||
|
const { account } = useParams({ strict: false });
|
||||||
|
|
||||||
if (guest) {
|
if (guest) {
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
<Popover.Root open={true}>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button type="button">
|
<button type="button">
|
||||||
<User.Provider pubkey={pubkey}>
|
<User.Provider pubkey={pubkey}>
|
||||||
@ -120,23 +124,20 @@ function Active({ pubkey }: { pubkey: string }) {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
||||||
Add account
|
Add account
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
|
||||||
⌘+Shift+N
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item
|
||||||
|
onClick={() => ark.open_profile(account)}
|
||||||
|
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
||||||
|
>
|
||||||
Profile
|
Profile
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
||||||
⌘+Shift+P
|
⌘+Shift+P
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item
|
||||||
Settings
|
onClick={() => navigate({ to: "/", search: { manually: true } })}
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
||||||
⌘+Shift+S
|
>
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
|
||||||
Logout
|
Logout
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
||||||
⌘+Shift+L
|
⌘+Shift+L
|
||||||
|
@ -14,6 +14,14 @@ export function LoginDialog() {
|
|||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!nsec.length) {
|
||||||
|
return toast.info("You must enter a valid nsec or ncrypto");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsec.startsWith("ncrypto") && !passphase.length) {
|
||||||
|
return toast.warning("You must provide a passphase for ncrypto key");
|
||||||
|
}
|
||||||
|
|
||||||
const save = await ark.save_account(nsec, passphase);
|
const save = await ark.save_account(nsec, passphase);
|
||||||
|
|
||||||
if (save) {
|
if (save) {
|
||||||
@ -81,6 +89,7 @@ export function LoginDialog() {
|
|||||||
<input
|
<input
|
||||||
name="nsec"
|
name="nsec"
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="nsec or ncrypto..."
|
||||||
value={nsec}
|
value={nsec}
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
onChange={(e) => setNsec(e.target.value)}
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
||||||
|
@ -46,6 +46,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => ark.open_settings()}
|
||||||
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
||||||
>
|
>
|
||||||
<HorizontalDotsIcon className="size-5" />
|
<HorizontalDotsIcon className="size-5" />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GlobalIcon, LocalIcon, RefreshIcon } from "@lume/icons";
|
import { GlobalIcon, LoaderIcon, LocalIcon, RefreshIcon } from "@lume/icons";
|
||||||
import { cn } from "@lume/utils";
|
import { cn } from "@lume/utils";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
@ -6,7 +6,7 @@ import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_all_accounts();
|
||||||
|
|
||||||
@ -22,9 +22,13 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
// Only 1 account, skip account selection screen
|
// Only 1 account, skip account selection screen
|
||||||
case 1:
|
case 1:
|
||||||
|
// @ts-ignore, totally fine !!!
|
||||||
|
if (search.manually) return;
|
||||||
|
|
||||||
const account = accounts[0].npub;
|
const account = accounts[0].npub;
|
||||||
const loadAccount = await ark.load_selected_account(account);
|
const loadedAccount = await ark.load_selected_account(account);
|
||||||
if (loadAccount) {
|
|
||||||
|
if (loadedAccount) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/$account/home/local",
|
to: "/$account/home/local",
|
||||||
params: { account },
|
params: { account },
|
||||||
@ -72,7 +76,9 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="size-6 animate-spin text-white" />
|
<div className="inline-flex size-6 items-center justify-center">
|
||||||
|
<LoaderIcon className="size-6 animate-spin text-white" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{ark.accounts.map((account) => (
|
{ark.accounts.map((account) => (
|
||||||
|
@ -569,4 +569,17 @@ export class Ark {
|
|||||||
fileDropEnabled: true,
|
fileDropEnabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public open_settings() {
|
||||||
|
return new WebviewWindow("settings", {
|
||||||
|
title: "Settings",
|
||||||
|
url: "/settings",
|
||||||
|
minWidth: 600,
|
||||||
|
width: 800,
|
||||||
|
height: 500,
|
||||||
|
hiddenTitle: true,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
fileDropEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function LoaderIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function LoaderIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg
|
||||||
<title id="loading">Loading</title>
|
{...props}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<circle
|
<circle
|
||||||
className="opacity-25"
|
className="opacity-25"
|
||||||
cx="12"
|
cx="12"
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { ReactNode, useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { User } from "../user";
|
import { User } from "../user";
|
||||||
import { Hashtag } from "./mentions/hashtag";
|
|
||||||
import { MentionUser } from "./mentions/user";
|
|
||||||
import { useEvent } from "@lume/ark";
|
import { useEvent } from "@lume/ark";
|
||||||
|
|
||||||
export function NoteChild({
|
export function NoteChild({
|
||||||
@ -18,66 +12,6 @@ export function NoteChild({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
const richContent = useMemo(() => {
|
|
||||||
if (!data) return "";
|
|
||||||
|
|
||||||
let parsedContent: string | ReactNode[] =
|
|
||||||
data.content.substring(0, 160) + "...";
|
|
||||||
|
|
||||||
const text = data.content;
|
|
||||||
const words = text.split(/( |\n)/);
|
|
||||||
|
|
||||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
|
||||||
const mentions = words.filter((word) =>
|
|
||||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hashtags.length) {
|
|
||||||
for (const hashtag of hashtags) {
|
|
||||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
|
||||||
parsedContent = reactStringReplace(parsedContent, regex, () => {
|
|
||||||
return <Hashtag key={nanoid()} tag={hashtag} />;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions.length) {
|
|
||||||
for (const mention of mentions) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
mention,
|
|
||||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
/(https?:\/\/\S+)/g,
|
|
||||||
(match, i) => {
|
|
||||||
const url = new URL(match);
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={match + i}
|
|
||||||
href={url.toString()}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="content-break font-normal text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{url.toString()}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedContent;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
return parsedContent;
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
@ -102,8 +36,8 @@ export function NoteChild({
|
|||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
<div className="relative flex-1 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="relative flex-1 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-100 dark:bg-neutral-900" />
|
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-100 dark:bg-neutral-900" />
|
||||||
<div className="content-break mt-6 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
<div className="content-break mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||||
{richContent}
|
{data.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<User.Provider pubkey={data.pubkey}>
|
<User.Provider pubkey={data.pubkey}>
|
||||||
|
@ -10,19 +10,18 @@ import {
|
|||||||
import { NIP89 } from "./nip89";
|
import { NIP89 } from "./nip89";
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
import { ReactNode, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { MentionUser } from "./mentions/user";
|
import { MentionUser } from "./mentions/user";
|
||||||
import { MentionNote } from "./mentions/note";
|
import { MentionNote } from "./mentions/note";
|
||||||
import { Hashtag } from "./mentions/hashtag";
|
import { Hashtag } from "./mentions/hashtag";
|
||||||
import { VideoPreview } from "./preview/video";
|
import { VideoPreview } from "./preview/video";
|
||||||
import { stripHtml } from "string-strip-html";
|
|
||||||
import { ImagePreview } from "./preview/image";
|
import { ImagePreview } from "./preview/image";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
|
||||||
export function NoteContent({ className }: { className?: string }) {
|
export function NoteContent({ className }: { className?: string }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
const text = stripHtml(event.content.trim()).result;
|
const text = event.content.trim();
|
||||||
const words = text.split(/( |\n)/);
|
const words = text.split(/( |\n)/);
|
||||||
|
|
||||||
// @ts-ignore, kaboom !!!
|
// @ts-ignore, kaboom !!!
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
|
||||||
import { ReactNode, useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { User } from "../../user";
|
import { User } from "../../user";
|
||||||
import { Hashtag } from "./hashtag";
|
|
||||||
import { MentionUser } from "./user";
|
|
||||||
import { useArk, useEvent } from "@lume/ark";
|
import { useArk, useEvent } from "@lume/ark";
|
||||||
import { LinkIcon } from "@lume/icons";
|
import { LinkIcon } from "@lume/icons";
|
||||||
import { stripHtml } from "string-strip-html";
|
|
||||||
|
|
||||||
export function MentionNote({
|
export function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
@ -16,72 +10,11 @@ export function MentionNote({
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
openable?: boolean;
|
openable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) return "";
|
|
||||||
|
|
||||||
const text = stripHtml(data.content.trim()).result;
|
|
||||||
const words = text.split(/( |\n)/);
|
|
||||||
|
|
||||||
// @ts-ignore, kaboom !!!
|
|
||||||
let parsedContent: ReactNode[] = text;
|
|
||||||
|
|
||||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
|
||||||
const mentions = words.filter((word) =>
|
|
||||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hashtags.length) {
|
|
||||||
for (const hashtag of hashtags) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
hashtag,
|
|
||||||
(match, i) => {
|
|
||||||
return <Hashtag key={match + i} tag={hashtag} />;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions.length) {
|
|
||||||
for (const mention of mentions) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
mention,
|
|
||||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
/(https?:\/\/\S+)/g,
|
|
||||||
(match, i) => {
|
|
||||||
const url = new URL(match);
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={match + i}
|
|
||||||
href={url.toString()}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="content-break inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{url.toString()}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedContent;
|
|
||||||
} catch (e) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -120,7 +53,7 @@ export function MentionNote({
|
|||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
|
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
|
||||||
{content}
|
{data.content}
|
||||||
</div>
|
</div>
|
||||||
{openable ? (
|
{openable ? (
|
||||||
<div className="flex h-10 items-center justify-between">
|
<div className="flex h-10 items-center justify-between">
|
||||||
|
Loading…
Reference in New Issue
Block a user