This commit is contained in:
Ren Amamiya 2023-06-22 10:45:45 +07:00
parent eaaf0e0e8a
commit 7b09dc3147
23 changed files with 285 additions and 124 deletions

View File

@ -2,7 +2,6 @@ import { Dialog, Transition } from "@headlessui/react";
import { createChannel } from "@libs/storage";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { AvatarUploader } from "@shared/avatarUploader";
import { Button } from "@shared/button";
import { CancelIcon, LoaderIcon, PlusIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider";
@ -83,11 +82,11 @@ export function ChannelCreateModal() {
onClick={() => openModal()}
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<PlusIcon width={12} height={12} className="text-zinc-500" />
</div>
<div>
<h5 className="font-medium text-zinc-400">Add a new channel</h5>
<h5 className="font-medium text-zinc-400">Create channel</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
@ -113,7 +112,7 @@ export function ChannelCreateModal() {
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border border-zinc-800 bg-zinc-900">
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
@ -168,32 +167,28 @@ export function ChannelCreateModal() {
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
Channel name *
</label>
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<input
type={"text"}
{...register("name", {
required: true,
minLength: 4,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<input
type={"text"}
{...register("name", {
required: true,
minLength: 4,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
Description
</label>
<div className="relative h-20 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<textarea
{...register("about")}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
<textarea
{...register("about")}
spellCheck={false}
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
/>
</div>
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
<div className="flex flex-col gap-1">
@ -218,13 +213,17 @@ export function ChannelCreateModal() {
</div>
</div>
<div>
<Button preset="large" disabled={!isDirty || !isValid}>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
"Create channel"
"Create channel"
)}
</Button>
</button>
</div>
</form>
</div>

View File

@ -1,4 +1,5 @@
import { useChannelProfile } from "@app/channel/hooks/useChannelProfile";
import { Link } from "@shared/link";
import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from "tailwind-merge";
@ -10,7 +11,7 @@ export function ChannelsListItem({ data }: { data: any }) {
const pageID = searchParams.id;
return (
<a
<Link
href={`/app/channel?id=${data.event_id}`}
className={twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
@ -19,7 +20,7 @@ export function ChannelsListItem({ data }: { data: any }) {
>
<div
className={twMerge(
"inline-flex shrink-0 h-5 w-5 items-center justify-center rounded bg-zinc-900",
"inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900",
pageID === data.event_id ? "bg-zinc-800" : "",
)}
>
@ -35,6 +36,6 @@ export function ChannelsListItem({ data }: { data: any }) {
)}
</div>
</div>
</a>
</Link>
);
}

View File

@ -1,4 +1,5 @@
import { Image } from "@shared/image";
import { Link } from "@shared/link";
import { DEFAULT_AVATAR } from "@stores/constants";
import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from "@utils/hooks/useProfile";
@ -17,13 +18,13 @@ export function ChatsListItem({ data }: { data: any }) {
{isError && <div>error</div>}
{isLoading && !user ? (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
</div>
</div>
) : (
<a
<Link
href={`/app/chat?pubkey=${data.sender_pubkey}`}
className={twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
@ -32,11 +33,11 @@ export function ChatsListItem({ data }: { data: any }) {
: "",
)}
>
<div className="relative h-5 w-5 shrink-0 rounded">
<div className="inline-flex shrink-0 h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<Image
src={user?.image || DEFAULT_AVATAR}
alt={data.sender_pubkey}
className="h-5 w-5 rounded object-cover"
className="h-6 w-6 rounded object-cover"
/>
</div>
<div className="w-full inline-flex items-center justify-between">
@ -55,7 +56,7 @@ export function ChatsListItem({ data }: { data: any }) {
)}
</div>
</div>
</a>
</Link>
)}
</>
);

View File

@ -1,4 +1,5 @@
import { ChatsListItem } from "@app/chat/components/item";
import { NewMessageModal } from "@app/chat/components/modal";
import { ChatsListSelfItem } from "@app/chat/components/self";
import { useActiveAccount } from "@stores/accounts";
import { useChats } from "@stores/chats";
@ -49,6 +50,7 @@ export function ChatsList() {
}
})
)}
<NewMessageModal />
</div>
);
}

View File

@ -0,0 +1,140 @@
import { Dialog, Transition } from "@headlessui/react";
import { getPlebs } from "@libs/storage";
import { CancelIcon, PlusIcon } from "@shared/icons";
import { DEFAULT_AVATAR } from "@stores/constants";
import { nip19 } from "nostr-tools";
import { Fragment, useState } from "react";
import useSWR from "swr";
import { navigate } from "vite-plugin-ssr/client/router";
const fetcher = () => getPlebs();
export function NewMessageModal() {
const [isOpen, setIsOpen] = useState(false);
const { data, isLoading }: any = useSWR("plebs", fetcher);
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
const openChat = (npub: string) => {
const pubkey = nip19.decode(npub).data;
navigate(`/app/chat?pubkey=${pubkey}`);
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<PlusIcon width={12} height={12} className="text-zinc-500" />
</div>
<div>
<h5 className="font-medium text-zinc-400">New chat</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
New chat
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={20}
height={20}
className="text-zinc-300"
/>
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
All messages will be encrypted, but anyone can see who you
chat
</Dialog.Description>
</div>
</div>
<div className="h-[500px] flex flex-col pb-5 overflow-y-auto">
{isLoading && <p>Loading...</p>}
{!data ? (
<p>Loading...</p>
) : (
data.map((pleb) => (
<div
key={pleb.npub}
className="group flex items-center justify-between px-4 py-3 hover:bg-zinc-800"
>
<div className="flex items-center gap-2">
<img
alt={pleb.npub}
src={pleb.image || DEFAULT_AVATAR}
className="w-9 h-9 shrink-0 object-cover rounded"
/>
<div className="inline-flex flex-col gap-1">
<h3 className="leading-none font-medium text-zinc-100">
{pleb.display_name || pleb.name}
</h3>
<span className="leading-none text-sm text-zinc-400">
{pleb.nip05 ||
pleb.npub.substring(0, 16).concat("...")}
</span>
</div>
</div>
<div>
<button
type="button"
onClick={() => openChat(pleb.npub)}
className="inline-flex text-sm w-max px-3 py-1.5 rounded border-t border-zinc-600/50 bg-zinc-700 hover:bg-fuchsia-500 transform translate-x-20 group-hover:translate-x-0 transition-transform ease-in-out duration-150"
>
Chat
</button>
</div>
</div>
))
)}
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,4 +1,5 @@
import { Image } from "@shared/image";
import { Link } from "@shared/link";
import { DEFAULT_AVATAR } from "@stores/constants";
import { usePageContext } from "@utils/hooks/usePageContext";
import { useProfile } from "@utils/hooks/useProfile";
@ -16,24 +17,24 @@ export function ChatsListSelfItem({ data }: { data: any }) {
<>
{isLoading && !user ? (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-5 w-5 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
</div>
</div>
) : (
<a
<Link
href={`/app/chat?pubkey=${data.pubkey}`}
className={twMerge(
"inline-flex h-9 items-center gap-2.5 rounded-md px-2.5",
pagePubkey === data.pubkey ? "bg-zinc-900 text-zinc-100" : "",
)}
>
<div className="relative h-5 w-5 shrink-0 rounded">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<Image
src={user?.image || DEFAULT_AVATAR}
alt={data.pubkey}
className="h-5 w-5 rounded bg-white object-cover"
className="h-6 w-6 rounded bg-white object-cover"
/>
</div>
<div className="inline-flex items-baseline gap-1">
@ -42,7 +43,7 @@ export function ChatsListSelfItem({ data }: { data: any }) {
</h5>
<span className="text-zinc-500">(you)</span>
</div>
</a>
</Link>
)}
</>
);

View File

@ -2,10 +2,17 @@ import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
import { nip19 } from "nostr-tools";
import { navigate } from "vite-plugin-ssr/client/router";
export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
const viewProfile = () => {
const pubkey = nip19.decode(user.npub).data;
navigate(`/app/user?pubkey=${pubkey}`);
};
return (
<div className="px-3 py-2">
<div className="flex flex-col gap-3">
@ -27,12 +34,13 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
</div>
<div>
<p className="leading-tight">{user?.bio || user?.about}</p>
<a
href={`/app/user?npub=${user.npub}`}
<button
type="button"
onClick={() => viewProfile()}
className="mt-3 inline-flex w-full h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800 text-sm text-zinc-300 hover:text-zinc-100 font-medium"
>
View full profile
</a>
</button>
</div>
</div>
</div>

View File

@ -63,10 +63,12 @@ export function Page() {
</div>
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl">
<div className="h-min w-full px-5 py-3">
<User
pubkey={account.pubkey}
time={Math.floor(Date.now() / 1000)}
/>
{account && (
<User
pubkey={account.pubkey}
time={Math.floor(Date.now() / 1000)}
/>
)}
<div className="-mt-6 pl-[49px] select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>Running Lume, fighting for better future</p>
<p>

View File

@ -2,6 +2,7 @@ import { getNotesByAuthor } from "@libs/storage";
import { CancelIcon } from "@shared/icons";
import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import { useActiveAccount } from "@stores/accounts";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useEffect, useMemo, useRef } from "react";
@ -56,20 +57,7 @@ export function FeedBlock({ params }: { params: any }) {
return (
<div className="shrink-0 w-[400px] border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
>
<div className="w-9 h-6" />
<h3 className="font-semibold text-zinc-100">{params.title}</h3>
<button
type="button"
onClick={() => close()}
className="inline-flex h-6 w-9 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800"
>
<CancelIcon width={14} height={14} className="text-zinc-500" />
</button>
</div>
<TitleBar title={params.title} onClick={() => close()} />
<div
ref={parentRef}
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"

View File

@ -3,6 +3,7 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { RelayContext } from "@shared/relayProvider";
import { TitleBar } from "@shared/titleBar";
import { useActiveAccount } from "@stores/accounts";
import { useVirtualizer } from "@tanstack/react-virtual";
import { dateToUnix } from "@utils/date";
@ -94,12 +95,7 @@ export function FollowingBlock({ block }: { block: number }) {
return (
<div className="shrink-0 w-[400px] border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full inline-flex items-center justify-center border-b border-zinc-900"
>
<h3 className="font-semibold text-zinc-100">Following</h3>
</div>
<TitleBar title="Circle" />
<div
ref={parentRef}
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"

View File

@ -1,5 +1,6 @@
import { CancelIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { TitleBar } from "@shared/titleBar";
import { useActiveAccount } from "@stores/accounts";
export function ImageBlock({ params }: { params: any }) {
@ -11,20 +12,7 @@ export function ImageBlock({ params }: { params: any }) {
return (
<div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
>
<div className="w-9 h-6" />
<h3 className="font-semibold text-zinc-100">{params.title}</h3>
<button
type="button"
onClick={() => close()}
className="inline-flex h-7 w-7 shrink items-center justify-center rounded bg-zinc-900 hover:bg-zinc-800"
>
<CancelIcon width={14} height={14} className="text-zinc-500" />
</button>
</div>
<TitleBar title={params.title} onClick={() => close()} />
<div className="w-full flex-1 p-3">
<Image
src={params.content}

View File

@ -6,6 +6,7 @@ import { NoteMetadata } from "@shared/notes/metadata";
import { NoteReplyForm } from "@shared/notes/replies/form";
import { RepliesList } from "@shared/notes/replies/list";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import { User } from "@shared/user";
import { useActiveAccount } from "@stores/accounts";
import { parser } from "@utils/parser";
@ -24,20 +25,7 @@ export function ThreadBlock({ params }: { params: any }) {
return (
<div className="shrink-0 w-[400px] border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
>
<button
type="button"
onClick={() => close()}
className="inline-flex h-7 w-7 shrink items-center justify-center rounded bg-zinc-900 hover:bg-zinc-800"
>
<ArrowLeftIcon width={14} height={14} className="text-zinc-500" />
</button>
<h3 className="font-semibold text-zinc-100">{params.title}</h3>
<div className="w-9 h-6" />
</div>
<TitleBar title={params.title} onClick={() => close()} />
<div className="scrollbar-hide flex w-full h-full flex-col gap-1.5 pt-1.5 pb-20 overflow-y-auto">
{!data ? (
<div className="px-3 py-1.5">

View File

@ -35,7 +35,7 @@ export function Profile({ data }: { data: any }) {
<h3 className="max-w-[15rem] truncate font-semibold text-zinc-100 leading-none">
{profile.display_name || profile.name}
</h3>
<p className="text-sm text-zinc-400 leading-none">
<p className="max-w-[10rem] truncate text-sm text-zinc-400 leading-none">
{profile.nip05 || shortenKey(data.pubkey)}
</p>
</div>

View File

@ -1,5 +1,6 @@
import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
@ -12,12 +13,7 @@ export function TrendingNotes() {
return (
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center justify-center px-3 border-b border-zinc-900"
>
<h3 className="font-semibold text-zinc-100">Trending Profiles</h3>
</div>
<TitleBar title="Trending Posts" />
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
{error && <p>Failed to load...</p>}
{!data ? (

View File

@ -1,5 +1,6 @@
import { Profile } from "@app/trending/components/profile";
import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
@ -12,12 +13,7 @@ export function TrendingProfiles() {
return (
<div className="shrink-0 w-[360px] flex-col flex border-r border-zinc-900">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center justify-center px-3 border-b border-zinc-900"
>
<h3 className="font-semibold text-zinc-100">Trending Profiles</h3>
</div>
<TitleBar title="Trending Profiles" />
<div className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto">
{error && <p>Failed to load...</p>}
{!data ? (

View File

@ -1,3 +1,4 @@
import { Link } from "@shared/link";
import { usePageContext } from "@utils/hooks/usePageContext";
import { twMerge } from "tailwind-merge";
@ -16,11 +17,11 @@ export function ActiveLink({
const pathName = pageContext.urlPathname;
return (
<a
<Link
href={href}
className={twMerge(className, href === pathName ? activeClassName : "")}
>
{children}
</a>
</Link>
);
}

View File

@ -1,8 +1,14 @@
import { DEFAULT_AVATAR } from "@stores/constants";
import { ClassAttributes, ImgHTMLAttributes, JSX } from "react";
export function Image(props) {
const addImageFallback = (event) => {
event.currentTarget.src = DEFAULT_AVATAR;
export function Image(
props: JSX.IntrinsicAttributes &
ClassAttributes<HTMLImageElement> &
ImgHTMLAttributes<HTMLImageElement>,
fallback = undefined,
) {
const addImageFallback = (event: { currentTarget: { src: string } }) => {
event.currentTarget.src = fallback ? fallback : DEFAULT_AVATAR;
};
return (

18
src/shared/link.tsx Normal file
View File

@ -0,0 +1,18 @@
import { ReactNode } from "react";
import { navigate } from "vite-plugin-ssr/client/router";
export function Link({
href,
className,
children,
}: { href: string; className?: string; children: ReactNode }) {
const goto = () => {
navigate(href, { keepScrollPosition: true });
};
return (
<button type="button" onClick={() => goto()} className={className}>
{children}
</button>
);
}

View File

@ -13,7 +13,7 @@ export function Navigation() {
return (
<div className="flex w-[232px] flex-col gap-3 border-r border-zinc-900">
<AppHeader />
<div className="flex flex-col gap-5 h-full overflow-y-auto scrollbar-hide">
<div className="flex flex-col gap-5 overflow-y-auto scrollbar-hide">
<div className="inlin-lflex h-8 px-3.5">
<Composer />
</div>
@ -30,7 +30,7 @@ export function Navigation() {
className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
activeClassName="bg-zinc-900/50"
>
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<SpaceIcon width={12} height={12} className="text-zinc-100" />
</span>
<span className="font-medium">Spaces</span>
@ -40,7 +40,7 @@ export function Navigation() {
className="flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200"
activeClassName="bg-zinc-900/50"
>
<span className="inline-flex h-5 w-5 items-center justify-center rounded bg-zinc-900">
<span className="inline-flex h-6 w-6 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<TrendingIcon
width={12}
height={12}

View File

@ -1,3 +1,4 @@
import { Link } from "@shared/link";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
@ -5,11 +6,11 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
return (
<a
<Link
href={`/user?pubkey=${pubkey}`}
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
>
@{user?.name || user?.displayName || shortenKey(pubkey)}
</a>
</Link>
);
}

27
src/shared/titleBar.tsx Normal file
View File

@ -0,0 +1,27 @@
import { CancelIcon } from "@shared/icons";
export function TitleBar({
title,
onClick = undefined,
}: { title: string; onClick?: () => void }) {
return (
<div
data-tauri-drag-region
className="group overflow-hidden h-11 w-full flex items-center justify-between px-3 border-b border-zinc-900"
>
<div className="w-6" />
<h3 className="text-sm font-medium text-zinc-200">{title}</h3>
{onClick ? (
<button
type="button"
onClick={onClick}
className="inline-flex h-6 w-6 shrink items-center justify-center rounded hover:bg-zinc-900 transform translate-y-8 group-hover:translate-y-0 transition-transform ease-in-out duration-150"
>
<CancelIcon width={12} height={12} className="text-zinc-300" />
</button>
) : (
<div className="w-6" />
)}
</div>
);
}

View File

@ -1,5 +1,6 @@
import { Popover, Transition } from "@headlessui/react";
import { Image } from "@shared/image";
import { Link } from "@shared/link";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useProfile } from "@utils/hooks/useProfile";
import { shortenKey } from "@utils/shortenKey";
@ -92,18 +93,18 @@ export function User({
</div>
</div>
<div className="flex items-center gap-2 px-3 py-3">
<a
<Link
href={`/app/user?pubkey=${pubkey}`}
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
>
View profile
</a>
<a
</Link>
<Link
href={`/app/chat?pubkey=${pubkey}`}
className="inline-flex h-10 flex-1 items-center justify-center rounded-md bg-zinc-700 hover:bg-fuchsia-500 text-sm font-medium"
>
Message
</a>
</Link>
</div>
</div>
</Popover.Panel>

View File

@ -1,3 +1,4 @@
import { Link } from "@shared/link";
import { MentionUser } from "@shared/notes/mentions/user";
import destr from "destr";
import getUrls from "get-urls";
@ -82,13 +83,13 @@ export function parser(event: any) {
// parse hashtag
content.parsed = reactStringReplace(content.parsed, /#(\w+)/g, (match, i) => (
<a
<Link
key={match + i}
href={`/search/${match}`}
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
>
#{match}
</a>
</Link>
));
return content;