mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
update
This commit is contained in:
@@ -1,42 +1,78 @@
|
||||
import { Component, createSignal, createMemo, Show, Switch, Match } from 'solid-js';
|
||||
import { Component, createSignal, createMemo, Show, Switch, Match, createEffect } from 'solid-js';
|
||||
|
||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
import CheckCircle from 'heroicons/24/solid/check-circle.svg';
|
||||
import ExclamationCircle from 'heroicons/24/solid/exclamation-circle.svg';
|
||||
import ArrowPath from 'heroicons/24/outline/arrow-path.svg';
|
||||
|
||||
import Modal from '@/components/Modal';
|
||||
import Timeline from '@/components/Timeline';
|
||||
import Copy from '@/components/utils/Copy';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import useFollowings from '@/nostr/useBatchedEvents';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import useVerification from '@/nostr/useVerification';
|
||||
import useFollowings from '@/nostr/useFollowings';
|
||||
import useFollowers from '@/nostr/useFollowers';
|
||||
import useConfig from '@/nostr/useConfig';
|
||||
import Timeline from './Timeline';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import epoch from '@/utils/epoch';
|
||||
|
||||
export type ProfileDisplayProps = {
|
||||
pubkey: string;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
||||
const { followersPubkeys } = useFollowers(() => ({
|
||||
pubkey: props.pubkey,
|
||||
}));
|
||||
|
||||
return <span>{followersPubkeys().length}</span>;
|
||||
};
|
||||
|
||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
const { config } = useConfig();
|
||||
const pubkey = usePubkey();
|
||||
|
||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||
const [showFollowers, setShowFollowers] = createSignal(false);
|
||||
|
||||
const { profile, query } = useProfile(() => ({
|
||||
const { profile, query: profileQuery } = useProfile(() => ({
|
||||
pubkey: props.pubkey,
|
||||
}));
|
||||
const { verification, query: verificationQuery } = useVerification(() =>
|
||||
ensureNonNull([profile()?.nip05] as const)(([nip05]) => ({ nip05 })),
|
||||
);
|
||||
const nip05Identifier = () => {
|
||||
const ident = profile()?.nip05;
|
||||
if (ident == null) return null;
|
||||
const [user, domain] = ident.split('@');
|
||||
if (domain == null) return null;
|
||||
if (user === '_') return { domain, ident: domain };
|
||||
return { user, domain, ident };
|
||||
};
|
||||
const isVerified = () => verification()?.pubkey === props.pubkey;
|
||||
|
||||
const { followingPubkeys: myFollowingPubkeys } = useFollowings(() =>
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
||||
pubkey: pubkeyNonNull,
|
||||
})),
|
||||
);
|
||||
const isFollowing = () => myFollowingPubkeys().includes(props.pubkey);
|
||||
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
||||
|
||||
const { followingPubkeys: userFollowingPubkeys } = useFollowings(() => ({
|
||||
pubkey: props.pubkey,
|
||||
}));
|
||||
const followed = () => {
|
||||
const p = pubkey();
|
||||
return p != null && userFollowingPubkeys().includes(p);
|
||||
};
|
||||
|
||||
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
||||
|
||||
@@ -47,7 +83,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
kinds: [1, 6],
|
||||
authors: [props.pubkey],
|
||||
limit: 10,
|
||||
until: Date.now() / 1000,
|
||||
until: epoch(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -64,17 +100,17 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
<XMark />
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex h-full flex-col overflow-y-scroll rounded-xl border bg-white text-stone-700 shadow-lg">
|
||||
<Show when={query.isFetched} fallback={<>loading</>}>
|
||||
<Show when={profile()?.banner} fallback={<div class="h-20 shrink-0" />} keyed>
|
||||
<div class="flex h-full flex-col overflow-y-scroll rounded-xl border bg-white pb-16 text-stone-700 shadow-lg">
|
||||
<Show when={profileQuery.isFetched} fallback={<>loading</>}>
|
||||
<Show when={profile()?.banner} fallback={<div class="h-12 shrink-0" />} keyed>
|
||||
{(bannerUrl) => (
|
||||
<div class="h-40 w-full shrink-0 sm:h-52">
|
||||
<img src={bannerUrl} alt="header" class="h-full w-full object-cover shadow" />
|
||||
<img src={bannerUrl} alt="header" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex h-[64px] items-center gap-4 px-4">
|
||||
<div class="mt-[-32px] h-28 w-28 shrink-0 rounded-lg shadow-md sm:mt-[-64px]">
|
||||
<div class="mt-[-54px] flex items-end gap-4 px-4 pt-4">
|
||||
<div class="h-28 w-28 shrink-0 rounded-lg shadow-md">
|
||||
<Show when={profile()?.picture} keyed>
|
||||
{(pictureUrl) => (
|
||||
<img
|
||||
@@ -85,60 +121,110 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mt-2 flex-1 overflow-hidden">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="truncate text-lg font-bold sm:text-xl">{profile()?.display_name}</div>
|
||||
<div class="truncate text-xs">@{profile()?.name}</div>
|
||||
<div class="flex items-start overflow-hidden">
|
||||
<div class="h-16 shrink overflow-hidden">
|
||||
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="truncate text-xl font-bold">{profile()?.display_name}</div>
|
||||
</Show>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={(profile()?.name?.length ?? 0) > 0}>
|
||||
<div class="truncate text-xs">@{profile()?.name}</div>
|
||||
</Show>
|
||||
<Show when={(profile()?.nip05?.length ?? 0) > 0}>
|
||||
<div class="flex items-center text-xs">
|
||||
{nip05Identifier()?.ident}
|
||||
<Switch
|
||||
fallback={
|
||||
<span class="inline-block h-4 w-4 text-rose-500">
|
||||
<ExclamationCircle />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Match when={verificationQuery.isLoading}>
|
||||
<span class="inline-block h-3 w-3">
|
||||
<ArrowPath />
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isVerified()}>
|
||||
<span class="inline-block h-4 w-4 text-blue-400">
|
||||
<CheckCircle />
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div class="truncate text-xs">{npub()}</div>
|
||||
<Copy
|
||||
class="h-4 w-4 shrink-0 text-stone-500 hover:text-stone-700"
|
||||
text={npub()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div class="truncate text-xs">{npub()}</div>
|
||||
<Copy
|
||||
class="h-4 w-4 shrink-0 text-stone-500 hover:text-stone-700"
|
||||
text={npub()}
|
||||
/>
|
||||
<div class="flex shrink-0 flex-col items-center justify-center gap-1">
|
||||
{/*
|
||||
<Switch
|
||||
fallback={
|
||||
<button
|
||||
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
フォロー
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Match when={props.pubkey === pubkey()}>
|
||||
<button
|
||||
class="w-20 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
編集
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={following()}>
|
||||
<button
|
||||
class="w-32 rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500"
|
||||
onMouseEnter={() => setHoverFollowButton(true)}
|
||||
onMouseLeave={() => setHoverFollowButton(false)}
|
||||
>
|
||||
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
||||
フォロー中
|
||||
</Show>
|
||||
</button>
|
||||
</Match>
|
||||
</Switch>
|
||||
*/}
|
||||
<Show when={followed()}>
|
||||
<div class="shrink-0 text-xs">フォローされています</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
<div>
|
||||
<Switch
|
||||
fallback={
|
||||
<button
|
||||
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
フォロー
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<Match when={props.pubkey === pubkey()}>
|
||||
<button
|
||||
class="w-20 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
編集
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={isFollowing()}>
|
||||
<button
|
||||
class="w-32 rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500"
|
||||
onMouseEnter={() => setHoverFollowButton(true)}
|
||||
onMouseLeave={() => setHoverFollowButton(false)}
|
||||
>
|
||||
<Show when={!hoverFollowButton()} fallback="フォロー解除">
|
||||
フォロー中
|
||||
</Show>
|
||||
</button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
<Show when={(profile()?.about ?? '').length > 0}>
|
||||
<div class="max-h-40 shrink-0 overflow-y-auto whitespace-pre-wrap px-5 py-4 text-sm">
|
||||
<div class="max-h-40 shrink-0 overflow-y-auto whitespace-pre-wrap px-5 py-3 text-sm">
|
||||
{profile()?.about}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex border-t px-4 py-2">
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<div class="text-sm">フォロー</div>
|
||||
<div class="text-xl">{userFollowingPubkeys().length}</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col items-start">
|
||||
<div class="text-sm">フォロワー</div>
|
||||
<div class="text-xl">
|
||||
<Show
|
||||
when={showFollowers()}
|
||||
fallback={<button onClick={() => setShowFollowers(true)}>読み込む</button>}
|
||||
keyed
|
||||
>
|
||||
<FollowersCount pubkey={props.pubkey} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={(profile()?.website ?? '').length > 0}>
|
||||
<ul class="border-t px-5 py-2 text-xs">
|
||||
<Show when={profile()?.website} keyed>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js';
|
||||
import { Show, For, createSignal, createMemo, onMount, type JSX, type Component } from 'solid-js';
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
import { createMutation } from '@tanstack/solid-query';
|
||||
|
||||
@@ -37,6 +37,8 @@ export type TextNoteDisplayProps = {
|
||||
};
|
||||
|
||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
let contentRef: HTMLDivElement | undefined;
|
||||
|
||||
const { config } = useConfig();
|
||||
const formatDate = useFormatDate();
|
||||
const pubkey = usePubkey();
|
||||
@@ -44,6 +46,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
|
||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||
const closeReplyForm = () => setShowReplyForm(false);
|
||||
const [showOverflow, setShowOverflow] = createSignal(false);
|
||||
const [overflow, setOverflow] = createSignal(false);
|
||||
const [showMenu, setShowMenu] = createSignal(false);
|
||||
|
||||
const event = createMemo(() => eventWrapper(props.event));
|
||||
@@ -91,8 +95,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const isReactedByMe = createMemo(() => isReactedBy(pubkey()));
|
||||
const isRepostedByMe = createMemo(() => isRepostedBy(pubkey()));
|
||||
const isReactedByMe = createMemo(() => {
|
||||
const p = pubkey();
|
||||
return p != null && isReactedBy(p);
|
||||
});
|
||||
const isRepostedByMe = createMemo(() => {
|
||||
const p = pubkey();
|
||||
return p != null && isRepostedBy(p);
|
||||
});
|
||||
|
||||
const showReplyEvent = (): string | undefined => {
|
||||
const replyingToEvent = event().replyingToEvent();
|
||||
@@ -141,6 +151,12 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (contentRef != null) {
|
||||
setOverflow(contentRef.scrollHeight > contentRef.clientHeight);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="nostr-textnote flex flex-col">
|
||||
<div class="flex w-full gap-1">
|
||||
@@ -177,33 +193,49 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</button>
|
||||
<div class="created-at shrink-0">{createdAt()}</div>
|
||||
</div>
|
||||
<Show when={showReplyEvent()} keyed>
|
||||
{(id) => (
|
||||
<div class="mt-1 rounded border p-1">
|
||||
<TextNoteDisplayById eventId={id} actions={false} embedding={false} />
|
||||
<div
|
||||
ref={contentRef}
|
||||
class="overflow-hidden"
|
||||
classList={{ 'max-h-screen': !showOverflow() }}
|
||||
>
|
||||
<Show when={showReplyEvent()} keyed>
|
||||
{(id) => (
|
||||
<div class="mt-1 rounded border p-1">
|
||||
<TextNoteDisplayById eventId={id} actions={false} embedding={false} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={event().mentionedPubkeys().length > 0}>
|
||||
<div class="text-xs">
|
||||
<For each={event().mentionedPubkeys()}>
|
||||
{(replyToPubkey: string) => (
|
||||
<button
|
||||
class="pr-1 text-blue-500 hover:underline"
|
||||
onClick={() => showProfile(replyToPubkey)}
|
||||
>
|
||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
{'への返信'}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<ContentWarningDisplay contentWarning={event().contentWarning()}>
|
||||
<div class="content whitespace-pre-wrap break-all">
|
||||
<TextNoteContentDisplay event={props.event} embedding={embedding()} />
|
||||
</div>
|
||||
</ContentWarningDisplay>
|
||||
</div>
|
||||
<Show when={overflow()}>
|
||||
<button
|
||||
class="text-xs text-stone-600 hover:text-stone-800"
|
||||
onClick={() => setShowOverflow((current) => !current)}
|
||||
>
|
||||
<Show when={!showOverflow()} fallback="隠す">
|
||||
続きを読む
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={event().mentionedPubkeys().length > 0}>
|
||||
<div class="text-xs">
|
||||
<For each={event().mentionedPubkeys()}>
|
||||
{(replyToPubkey: string) => (
|
||||
<button
|
||||
class="pr-1 text-blue-500 hover:underline"
|
||||
onClick={() => showProfile(replyToPubkey)}
|
||||
>
|
||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
{'への返信'}
|
||||
</div>
|
||||
</Show>
|
||||
<ContentWarningDisplay contentWarning={event().contentWarning()}>
|
||||
<div class="content whitespace-pre-wrap break-all">
|
||||
<TextNoteContentDisplay event={props.event} embedding={embedding()} />
|
||||
</div>
|
||||
</ContentWarningDisplay>
|
||||
<Show when={actions()}>
|
||||
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
||||
<button
|
||||
|
||||
@@ -29,7 +29,7 @@ const Copy: Component<CopyProps> = (props) => {
|
||||
</button>
|
||||
<Show when={showPopup()}>
|
||||
<div
|
||||
class="absolute left-[-1rem] top-[-1.5rem] rounded
|
||||
class="absolute left-[-2.5rem] top-[-1.5rem] rounded
|
||||
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
||||
>
|
||||
Copied!
|
||||
|
||||
Reference in New Issue
Block a user