Settings Manager (#211)
* refactor: landing screen * fix: code debt * feat: add settings screen * chore: clean up * feat: settings * feat: small updates
This commit is contained in:
@@ -44,9 +44,9 @@ export function Column({
|
||||
useEffect(() => {
|
||||
if (!isCreated) return;
|
||||
|
||||
const unlisten = listen<WindowEvent>("window", (data) => {
|
||||
const unlisten = listen<WindowEvent>("child-webview", (data) => {
|
||||
if (data.payload.scroll) repositionWebview();
|
||||
if (data.payload.resize) resizeWebview();
|
||||
if (data.payload.resize) repositionWebview().then(() => resizeWebview());
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { RepostIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { LumeWindow } from "@lume/system";
|
||||
import { Spinner } from "@lume/ui";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteRepost({ large = false }: { large?: boolean }) {
|
||||
const event = useNoteContext();
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isRepost, setIsRepost] = useState(false);
|
||||
@@ -56,6 +57,8 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
||||
await menu.popup().catch((e) => console.error(e));
|
||||
}, []);
|
||||
|
||||
if (!settings.display_repost_button) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { ZapIcon } from "@lume/icons";
|
||||
import { useNoteContext } from "../provider";
|
||||
import { cn } from "@lume/utils";
|
||||
import { LumeWindow } from "@lume/system";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { useNoteContext } from "../provider";
|
||||
|
||||
export function NoteZap({ large = false }: { large?: boolean }) {
|
||||
const event = useNoteContext();
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
if (!settings.display_zap_button) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { nanoid } from "nanoid";
|
||||
import { type ReactNode, useMemo } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { Hashtag } from "./mentions/hashtag";
|
||||
@@ -7,7 +9,6 @@ import { MentionUser } from "./mentions/user";
|
||||
import { Images } from "./preview/images";
|
||||
import { Videos } from "./preview/videos";
|
||||
import { useNoteContext } from "./provider";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export function NoteContent({
|
||||
quote = true,
|
||||
@@ -20,6 +21,7 @@ export function NoteContent({
|
||||
clean?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
const event = useNoteContext();
|
||||
const content = useMemo(() => {
|
||||
try {
|
||||
@@ -27,7 +29,9 @@ export function NoteContent({
|
||||
const { content, hashtags, events, mentions } = event.meta;
|
||||
|
||||
// Define rich content
|
||||
let richContent: ReactNode[] | string = content;
|
||||
let richContent: ReactNode[] | string = settings.display_media
|
||||
? content
|
||||
: event.content;
|
||||
|
||||
for (const hashtag of hashtags) {
|
||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
||||
@@ -98,8 +102,12 @@ export function NoteContent({
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{event.meta?.images.length ? <Images urls={event.meta.images} /> : null}
|
||||
{event.meta?.videos.length ? <Videos urls={event.meta.videos} /> : null}
|
||||
{settings.display_media && event.meta?.images.length ? (
|
||||
<Images urls={event.meta.images} />
|
||||
) : null}
|
||||
{settings.display_media && event.meta?.videos.length ? (
|
||||
<Videos urls={event.meta.videos} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function ImagePreview({ url }: { url: string }) {
|
||||
const open = async (url: string) => {
|
||||
const name = new URL(url).pathname
|
||||
.split("/")
|
||||
.pop()
|
||||
.replace(/[^a-zA-Z ]/g, "");
|
||||
const label = `viewer-${name}`;
|
||||
const window = WebviewWindow.getByLabel(label);
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
if (!window) {
|
||||
const newWindow = new WebviewWindow(label, {
|
||||
url,
|
||||
title: "Image Viewer",
|
||||
width: 800,
|
||||
height: 800,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
return newWindow;
|
||||
const imageUrl = useMemo(() => {
|
||||
if (settings.image_resize_service.length) {
|
||||
const newUrl = `${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`;
|
||||
return newUrl;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}, [settings.image_resize_service]);
|
||||
|
||||
return await window.setFocus();
|
||||
};
|
||||
if (!settings.display_media) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative my-1 group">
|
||||
<div className="my-1">
|
||||
<img
|
||||
src={url}
|
||||
src={imageUrl}
|
||||
alt={url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { Carousel, CarouselItem } from "@lume/ui";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function Images({ urls }: { urls: string[] }) {
|
||||
const open = async (url: string) => {
|
||||
const name = new URL(url).pathname
|
||||
.split("/")
|
||||
.pop()
|
||||
.replace(/[^a-zA-Z ]/g, "");
|
||||
const label = `viewer-${name}`;
|
||||
const window = WebviewWindow.getByLabel(label);
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
if (!window) {
|
||||
const newWindow = new WebviewWindow(label, {
|
||||
url,
|
||||
title: "Image Viewer",
|
||||
width: 800,
|
||||
height: 800,
|
||||
titleBarStyle: "overlay",
|
||||
});
|
||||
|
||||
return newWindow;
|
||||
const imageUrls = useMemo(() => {
|
||||
if (settings.image_resize_service.length) {
|
||||
const newUrls = urls.map(
|
||||
(url) =>
|
||||
`${settings.image_resize_service}?url=${url}&ll&af&default=1&n=-1`,
|
||||
);
|
||||
return newUrls;
|
||||
} else {
|
||||
return urls;
|
||||
}
|
||||
|
||||
return await window.setFocus();
|
||||
};
|
||||
}, [settings.image_resize_service]);
|
||||
|
||||
if (urls.length === 1) {
|
||||
return (
|
||||
<div className="px-3 group">
|
||||
<img
|
||||
src={urls[0]}
|
||||
src={imageUrls[0]}
|
||||
alt={urls[0]}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={{ contentVisibility: "auto" }}
|
||||
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
|
||||
onClick={() => open(urls[0])}
|
||||
onKeyDown={() => open(urls[0])}
|
||||
onClick={() => urls[0]}
|
||||
onKeyDown={() => urls[0]}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
currentTarget.src = "/404.jpg";
|
||||
@@ -48,7 +41,7 @@ export function Images({ urls }: { urls: string[] }) {
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
items={urls}
|
||||
items={imageUrls}
|
||||
renderItem={({ item, isSnapPoint }) => (
|
||||
<CarouselItem key={item} isSnapPoint={isSnapPoint}>
|
||||
<img
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useOpenGraph } from "@lume/utils";
|
||||
|
||||
function isImage(url: string) {
|
||||
return /^https?:\/\/.+\.(jpg|jpeg|png|webp|avif)$/.test(url);
|
||||
}
|
||||
|
||||
export function LinkPreview({ url }: { url: string }) {
|
||||
const domain = new URL(url);
|
||||
const { isLoading, isError, data } = useOpenGraph(url);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="my-1.5 flex w-full flex-col overflow-hidden rounded-2xl border border-black/10 p-3 dark:border-white/10">
|
||||
<div className="h-48 w-full shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="flex flex-col gap-2 px-3 py-3">
|
||||
<div className="h-3 w-2/3 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<div className="h-3 w-3/4 animate-pulse rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||
<span className="mt-2.5 text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
||||
{domain.hostname}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.title && !data.image && !data.description) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-block text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-block text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="my-1 flex w-full flex-col overflow-hidden rounded-2xl border border-black/10 dark:border-white/10"
|
||||
>
|
||||
{isImage(data.image) ? (
|
||||
<img
|
||||
src={data.image}
|
||||
alt={url}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-48 w-full shrink-0 rounded-t-lg bg-white object-cover"
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex flex-col items-start p-3">
|
||||
<div className="flex flex-col items-start text-left">
|
||||
{data.title ? (
|
||||
<div className="content-break line-clamp-1 text-base font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{data.title}
|
||||
</div>
|
||||
) : null}
|
||||
{data.description ? (
|
||||
<div className="content-break mb-2 line-clamp-3 text-balance text-sm text-neutral-700 dark:text-neutral-400">
|
||||
{data.description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="break-all text-sm font-semibold text-blue-500">
|
||||
{domain.hostname}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
|
||||
export function VideoPreview({ url }: { url: string }) {
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
if (settings.display_media) {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-1">
|
||||
<video
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { useRouteContext } from "@tanstack/react-router";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useMemo } from "react";
|
||||
@@ -7,19 +8,46 @@ import { useUserContext } from "./provider";
|
||||
|
||||
export function UserAvatar({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
const { settings } = useRouteContext({ strict: false });
|
||||
|
||||
const picture = useMemo(() => {
|
||||
if (
|
||||
settings?.image_resize_service?.length &&
|
||||
user.profile?.picture?.length
|
||||
) {
|
||||
const url = `${settings.image_resize_service}?url=${user.profile?.picture}&w=100&h=100&default=1&n=-1`;
|
||||
return url;
|
||||
} else {
|
||||
return user.profile?.picture;
|
||||
}
|
||||
}, [user.profile?.picture]);
|
||||
|
||||
const fallbackAvatar = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(user.pubkey || nanoid(), 90, 50),
|
||||
)}`,
|
||||
[user],
|
||||
[user.pubkey],
|
||||
);
|
||||
|
||||
if (settings && !settings.display_avatar) {
|
||||
return (
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Fallback delayMs={120}>
|
||||
<img
|
||||
src={fallbackAvatar}
|
||||
alt={user.pubkey}
|
||||
className={cn("bg-black dark:bg-white", className)}
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar.Root className="shrink-0">
|
||||
<Avatar.Image
|
||||
src={user.profile?.picture}
|
||||
src={picture}
|
||||
alt={user.pubkey}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
|
||||
@@ -15,7 +15,7 @@ export function UserCover({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (user && !user.profile.banner) {
|
||||
if (user && !user.profile?.banner) {
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-gradient-to-b from-blue-400 to-teal-200", className)}
|
||||
@@ -25,7 +25,7 @@ export function UserCover({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<img
|
||||
src={user.profile.banner}
|
||||
src={user?.profile?.banner}
|
||||
alt="banner"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
|
||||
Reference in New Issue
Block a user