This commit is contained in:
reya 2024-01-21 09:43:46 +07:00
parent 446721729b
commit f09139ffbe
15 changed files with 103 additions and 125 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View File

@ -197,10 +197,6 @@ export function CreateAccountScreen() {
<h1 className="text-2xl font-semibold">
Let's get you set up on Nostr.
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Get started with familiar way, but all data belong to you and you
have ability controls everything.
</p>
</div>
{!services ? (
<div className="flex items-center justify-center w-full">
@ -220,59 +216,75 @@ export function CreateAccountScreen() {
>
Username *
</label>
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="lume"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root value={serviceId} onValueChange={setServiceId}>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Public handles
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="alice"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root
value={serviceId}
onValueChange={setServiceId}
>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>
@{getDomainName(serviceId)}
</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Choose a Provider
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
<span className="text-sm text-neutral-600">
Use to login to Lume and other Nostr apps. You can choose
provider you trust to manage your account
</span>
</div>
</div>
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (Optional)
</label>
<input
type={"email"}
{...register("email", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
<div className="flex flex-col gap-1.5">
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (optional)
</label>
<input
type={"email"}
{...register("email", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
</div>
<span className="text-sm text-neutral-600">
Use for recover your account if you lose your password
</span>
</div>
</div>
<div>
@ -297,12 +309,12 @@ export function CreateAccountScreen() {
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-500">
Or
Or manage your own keys
</span>
</div>
</div>
<span className="mx-auto text-xs font-medium bg-black text-neutral-600">
More compatible with other Nostr clients
Mostly compatible with other Nostr clients
</span>
</div>
<div>

View File

@ -23,9 +23,9 @@ export function WelcomeScreen() {
className="w-2/3"
/>
<p className="mt-5 text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Lume is your safe Nostr client to meet, explore and
Lume is a magnificent client for Nostr to meet, explore
<br />
freely sharing your though to everyone in nostrverse
and freely share your thoughts with everyone.
</p>
</div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto">

View File

@ -1,17 +1,14 @@
import { useRelaylist } from "@lume/ark";
import { PlusIcon } from "@lume/icons";
import { NDKRelayUrl } from "@nostr-dev-kit/ndk";
import { normalizeRelayUrl } from "nostr-fetch";
import { useState } from "react";
import { toast } from "sonner";
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
export function RelayForm() {
const { connectRelay } = useRelaylist();
const [relay, setRelay] = useState<{
url: NDKRelayUrl;
url: WebSocket["url"];
purpose: "read" | "write" | undefined;
}>({ url: "", purpose: undefined });
@ -19,10 +16,8 @@ export function RelayForm() {
if (relay.url.length < 1) return toast.info("Please enter relay url");
try {
const relayUrl = new URL(relay.url.replace(/\s/g, ""));
if (
domainRegex.test(relayUrl.host) &&
(relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:")
) {
if (relayUrl.protocol === "wss:" || relayUrl.protocol === "ws:") {
connectRelay.mutate(normalizeRelayUrl(relay.url));
setRelay({ url: "", purpose: undefined });
} else {

View File

@ -9,6 +9,7 @@ import {
useEffect,
useState,
} from "react";
import { toast } from "sonner";
type ColumnContext = {
columns: IColumn[];
@ -52,6 +53,11 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
}, []);
const removeColumn = useCallback(async (id: number) => {
if (id === 9998 || id === 9999) {
toast.info("You cannot remove default column");
return;
}
await storage.removeColumn(id);
setColumns((prev) => prev.filter((t) => t.id !== id));
}, []);

View File

@ -1,4 +1,5 @@
import { NOSTR_MENTIONS } from "@lume/utils";
import { nanoid } from "nanoid";
import { nip19 } from "nostr-tools";
import { ReactNode, useMemo } from "react";
import { Link } from "react-router-dom";
@ -33,13 +34,10 @@ export function NoteChild({
try {
if (hashtags.length) {
for (const hashtag of hashtags) {
parsedContent = reactStringReplace(
parsedContent,
hashtag,
(match, i) => {
return <Hashtag key={match + i} tag={hashtag} />;
},
);
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
parsedContent = reactStringReplace(parsedContent, regex, () => {
return <Hashtag key={nanoid()} tag={hashtag} />;
});
}
}

View File

@ -126,15 +126,12 @@ export function NoteContent({
if (hashtags.length) {
for (const hashtag of hashtags) {
parsedContent = reactStringReplace(
parsedContent,
hashtag,
(match, i) => {
if (storage.settings.hashtag)
return <Hashtag key={match + i} tag={hashtag} />;
return null;
},
);
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
parsedContent = reactStringReplace(parsedContent, regex, () => {
if (storage.settings.hashtag)
return <Hashtag key={nanoid()} tag={hashtag} />;
return null;
});
}
}

View File

@ -13,7 +13,7 @@ export function ImagePreview({ url }: { url: string }) {
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf("/") + 1);
await download(url, downloadDirPath + `/${filename}`);
await download(url, `${downloadDirPath}/${filename}`);
setDownloaded(true);
} catch (e) {
@ -35,7 +35,7 @@ export function ImagePreview({ url }: { url: string }) {
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div onClick={open} className="relative my-1 group">
<div onClick={open} className="relative mt-1 mb-2.5 group">
<img
src={url}
alt={url}
@ -43,7 +43,7 @@ export function ImagePreview({ url }: { url: string }) {
decoding="async"
style={{ contentVisibility: "auto" }}
onError={fallback}
className="object-cover w-full h-auto border rounded-lg border-neutral-200/50 dark:border-neutral-800/50"
className="object-cover w-full h-auto border rounded-xl border-neutral-200/50 dark:border-neutral-800/50"
/>
<button
type="button"

View File

@ -11,7 +11,7 @@ export function LinkPreview({ url }: { url: string }) {
if (status === "pending") {
return (
<div className="flex flex-col w-full my-1 rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5">
<div className="flex flex-col w-full mt-1 mb-2.5 rounded-xl overflow-hidden bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5">
<div className="w-full h-48 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="w-2/3 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
@ -42,7 +42,7 @@ export function LinkPreview({ url }: { url: string }) {
to={url}
target="_blank"
rel="noreferrer"
className="flex flex-col w-full my-1 overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5"
className="flex flex-col w-full mt-1 mb-2.5 overflow-hidden rounded-xl bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5"
>
{isImage(data.image) ? (
<img
@ -53,8 +53,8 @@ export function LinkPreview({ url }: { url: string }) {
className="object-cover w-full h-48 bg-white rounded-t-lg"
/>
) : null}
<div className="flex flex-col items-start px-3 py-3">
<div className="flex flex-col items-start gap-1 text-left">
<div className="flex flex-col items-start p-3">
<div className="flex flex-col items-start text-left">
{data.title ? (
<div className="text-base font-semibold break-p text-neutral-900 dark:text-neutral-100">
{data.title}
@ -66,7 +66,7 @@ export function LinkPreview({ url }: { url: string }) {
</div>
) : null}
</div>
<div className="text-sm break-all text-neutral-600 dark:text-neutral-400">
<div className="text-sm break-all text-blue-500 font-semibold">
{domain.hostname}
</div>
</div>

View File

@ -10,7 +10,7 @@ import {
export function VideoPreview({ url }: { url: string }) {
return (
<div className="my-1 w-full rounded-lg overflow-hidden">
<div className="mt-1 mb-2.5 w-full rounded-xl overflow-hidden">
<MediaController>
<video
slot="media"

View File

@ -17,7 +17,7 @@ export function ComposeIcon(
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M20 14c0 2.8 0 4.2-.545 5.27a5 5 0 01-2.185 2.185C16.2 22 14.8 22 12 22h-2c-2.8 0-4.2 0-5.27-.545a5 5 0 01-2.185-2.185C2 18.2 2 16.8 2 14v-2c0-2.8 0-4.2.545-5.27A5 5 0 014.73 4.545C5.8 4 7.2 4 10 4m-1.938 9.502c.008-.351.013-.527.055-.691.038-.146.097-.286.177-.414.09-.144.213-.268.46-.517l9.396-9.45a1.46 1.46 0 011.828-.196A5.87 5.87 0 0121.7 3.946l.075.116c.376.605.253 1.371-.24 1.867l-9.322 9.375c-.257.257-.385.386-.534.478a1.476 1.476 0 01-.429.178c-.17.04-.351.04-.714.04L8 15.996l.062-2.495z"
d="M11.162 15.312L13.295 22H8.92l-2.116-6.044m4.358-.644c-1.458.166-2.907.423-4.358.644m4.358-.644a27.427 27.427 0 018.16.296m-12.518.348l-1.632.248a5.126 5.126 0 01-1.699-6.34l4.67-1.824a27.428 27.428 0 007.882-4.733m3.296 12.301c.157.09.303.121.436.086.876-.235.825-3.263-.114-6.765-.938-3.5-2.408-6.15-3.283-5.915-.133.036-.245.136-.335.293m3.296 12.301c-.873-.504-2.052-2.86-2.847-5.83-.796-2.969-.953-5.598-.449-6.471"
/>
</svg>
);

View File

@ -14,11 +14,7 @@ export function ComposeFilledIcon(
>
<path
fill="currentColor"
d="M22.537 3.398a6.87 6.87 0 00-2.017-2.004 2.46 2.46 0 00-3.079.331l-9.396 9.45-.047.047c-.2.2-.402.403-.554.648a2.49 2.49 0 00-.295.691c-.072.28-.079.566-.086.85L7 15.97a1 1 0 00.998 1.025l2.538.004h.068c.293.001.59.002.88-.068.254-.06.497-.161.72-.298.253-.156.462-.367.668-.574h.001l.05-.05 9.322-9.376c.791-.795 1.021-2.067.381-3.098-.024-.04-.054-.085-.073-.113l-.005-.008-.01-.017z"
/>
<path
fill="currentColor"
d="M5.184 5.436c.37-.189.842-.308 1.613-.371L10 5a1 1 0 100-2h-.044c-1.363 0-2.447 0-3.321.071-.896.074-1.66.227-2.359.583a6 6 0 00-2.622 2.622c-.356.7-.51 1.463-.583 2.359C1 9.509 1 10.593 1 11.956v2.088c0 1.363 0 2.447.071 3.322.074.895.227 1.659.583 2.358a6 6 0 002.622 2.622c.7.356 1.463.51 2.359.583C7.509 23 8.593 23 9.956 23h2.088c1.363 0 2.447 0 3.321-.071.896-.074 1.66-.227 2.359-.583a5.998 5.998 0 002.622-2.622c.356-.7.51-1.463.583-2.358.071-.875.071-1.96.071-3.322V14a1 1 0 10-2 0c0 1.417 0 2.419-.065 3.203-.063.771-.182 1.243-.371 1.613a4 4 0 01-1.748 1.748c-.37.189-.841.308-1.613.371C14.419 21 13.417 21 12 21h-2c-1.417 0-2.419 0-3.203-.065-.771-.063-1.243-.182-1.613-.371a4 4 0 01-1.748-1.748c-.189-.37-.308-.841-.371-1.613C3 16.419 3 15.417 3 14v-2c0-1.417 0-2.419.065-3.203.063-.771.182-1.243.371-1.613a4 4 0 011.748-1.748z"
d="M15.27 2.637a1.51 1.51 0 01.831-.589c.771-.206 1.39.273 1.71.585.373.366.723.879 1.042 1.452.644 1.16 1.273 2.78 1.756 4.585.484 1.806.75 3.522.771 4.85.012.655-.035 1.274-.176 1.779-.12.43-.417 1.154-1.188 1.36a1.51 1.51 0 01-1.015-.093 26.427 26.427 0 00-6.508-.37l1.755 5.5A1 1 0 0113.296 23H8.92a1 1 0 01-.944-.67L6.135 17.07l-.812.124a1 1 0 01-.727-.172 6.126 6.126 0 01-2.03-7.576 1 1 0 01.544-.512l4.67-1.824a26.429 26.429 0 007.49-4.472zm4.093 11.584c.015-.189.022-.411.018-.668-.02-1.12-.249-2.67-.703-4.365-.455-1.696-1.03-3.152-1.574-4.132a6.928 6.928 0 00-.35-.57c-.026.325-.026.73.006 1.204.07 1.06.295 2.395.68 3.83.384 1.434.857 2.702 1.326 3.656.21.427.412.777.597 1.045zM9.629 21h2.298l-1.46-4.575-2.319.343L9.63 21z"
/>
</svg>
);

View File

@ -14,33 +14,6 @@ export function Default({ column }: { column: IColumn }) {
icon={<ColumnIcon className="size-4" />}
/>
<div className="h-full px-3 mt-3 flex flex-col gap-3 overflow-y-auto scrollbar-none">
<div className="flex flex-col rounded-xl overflow-hidden">
<div className="h-[100px] w-full">
<img
src="/columns/topic.jpg"
srcSet="/columns/topic@2x.jpg 2x"
alt="topic"
className="w-full h-auto object-cover"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between bg-neutral-50 dark:bg-neutral-950">
<div>
<h1 className="font-semibold">Topic</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-500 dark:text-neutral-600">
Explore all content based on your interest.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({ kind: COL_TYPES.topic, title: "", content: "" });
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="flex flex-col rounded-xl overflow-hidden">
<div className="h-[100px] w-full">
<img

View File

@ -16,6 +16,7 @@ export function OnboardingFinishScreen() {
setLoading(true);
await queryClient.refetchQueries({ queryKey: ["timeline-9999"] });
await queryClient.refetchQueries({ queryKey: ["foryou-9998"] });
setLoading(false);
setOnboarding({ open: false, newUser: false });