mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 06:24: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 GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||||
import XMark from 'heroicons/24/outline/x-mark.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 Modal from '@/components/Modal';
|
||||||
|
import Timeline from '@/components/Timeline';
|
||||||
import Copy from '@/components/utils/Copy';
|
import Copy from '@/components/utils/Copy';
|
||||||
import SafeLink from '@/components/utils/SafeLink';
|
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 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 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 = {
|
export type ProfileDisplayProps = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
onClose?: () => void;
|
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 ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
|
|
||||||
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
const [hoverFollowButton, setHoverFollowButton] = createSignal(false);
|
||||||
|
const [showFollowers, setShowFollowers] = createSignal(false);
|
||||||
|
|
||||||
const { profile, query } = useProfile(() => ({
|
const { profile, query: profileQuery } = useProfile(() => ({
|
||||||
pubkey: props.pubkey,
|
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(() =>
|
const { followingPubkeys: myFollowingPubkeys } = useFollowings(() =>
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({
|
||||||
pubkey: 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));
|
const npub = createMemo(() => npubEncodeFallback(props.pubkey));
|
||||||
|
|
||||||
@@ -47,7 +83,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
kinds: [1, 6],
|
kinds: [1, 6],
|
||||||
authors: [props.pubkey],
|
authors: [props.pubkey],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
until: Date.now() / 1000,
|
until: epoch(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
@@ -64,17 +100,17 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
<XMark />
|
<XMark />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex h-full flex-col overflow-y-scroll rounded-xl border bg-white text-stone-700 shadow-lg">
|
<div class="flex h-full flex-col overflow-y-scroll rounded-xl border bg-white pb-16 text-stone-700 shadow-lg">
|
||||||
<Show when={query.isFetched} fallback={<>loading</>}>
|
<Show when={profileQuery.isFetched} fallback={<>loading</>}>
|
||||||
<Show when={profile()?.banner} fallback={<div class="h-20 shrink-0" />} keyed>
|
<Show when={profile()?.banner} fallback={<div class="h-12 shrink-0" />} keyed>
|
||||||
{(bannerUrl) => (
|
{(bannerUrl) => (
|
||||||
<div class="h-40 w-full shrink-0 sm:h-52">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex h-[64px] items-center gap-4 px-4">
|
<div class="mt-[-54px] flex items-end gap-4 px-4 pt-4">
|
||||||
<div class="mt-[-32px] h-28 w-28 shrink-0 rounded-lg shadow-md sm:mt-[-64px]">
|
<div class="h-28 w-28 shrink-0 rounded-lg shadow-md">
|
||||||
<Show when={profile()?.picture} keyed>
|
<Show when={profile()?.picture} keyed>
|
||||||
{(pictureUrl) => (
|
{(pictureUrl) => (
|
||||||
<img
|
<img
|
||||||
@@ -85,60 +121,110 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex-1 overflow-hidden">
|
<div class="flex items-start overflow-hidden">
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="h-16 shrink overflow-hidden">
|
||||||
<div class="truncate text-lg font-bold sm:text-xl">{profile()?.display_name}</div>
|
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
||||||
<div class="truncate text-xs">@{profile()?.name}</div>
|
<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>
|
||||||
<div class="flex gap-1">
|
<div class="flex shrink-0 flex-col items-center justify-center gap-1">
|
||||||
<div class="truncate text-xs">{npub()}</div>
|
{/*
|
||||||
<Copy
|
<Switch
|
||||||
class="h-4 w-4 shrink-0 text-stone-500 hover:text-stone-700"
|
fallback={
|
||||||
text={npub()}
|
<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>
|
</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>
|
</div>
|
||||||
<Show when={(profile()?.about ?? '').length > 0}>
|
<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}
|
{profile()?.about}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</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}>
|
<Show when={(profile()?.website ?? '').length > 0}>
|
||||||
<ul class="border-t px-5 py-2 text-xs">
|
<ul class="border-t px-5 py-2 text-xs">
|
||||||
<Show when={profile()?.website} keyed>
|
<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 type { Event as NostrEvent } from 'nostr-tools';
|
||||||
import { createMutation } from '@tanstack/solid-query';
|
import { createMutation } from '@tanstack/solid-query';
|
||||||
|
|
||||||
@@ -37,6 +37,8 @@ export type TextNoteDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||||
|
let contentRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
@@ -44,6 +46,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
|
|
||||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||||
const closeReplyForm = () => setShowReplyForm(false);
|
const closeReplyForm = () => setShowReplyForm(false);
|
||||||
|
const [showOverflow, setShowOverflow] = createSignal(false);
|
||||||
|
const [overflow, setOverflow] = createSignal(false);
|
||||||
const [showMenu, setShowMenu] = createSignal(false);
|
const [showMenu, setShowMenu] = createSignal(false);
|
||||||
|
|
||||||
const event = createMemo(() => eventWrapper(props.event));
|
const event = createMemo(() => eventWrapper(props.event));
|
||||||
@@ -91,8 +95,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isReactedByMe = createMemo(() => isReactedBy(pubkey()));
|
const isReactedByMe = createMemo(() => {
|
||||||
const isRepostedByMe = createMemo(() => isRepostedBy(pubkey()));
|
const p = pubkey();
|
||||||
|
return p != null && isReactedBy(p);
|
||||||
|
});
|
||||||
|
const isRepostedByMe = createMemo(() => {
|
||||||
|
const p = pubkey();
|
||||||
|
return p != null && isRepostedBy(p);
|
||||||
|
});
|
||||||
|
|
||||||
const showReplyEvent = (): string | undefined => {
|
const showReplyEvent = (): string | undefined => {
|
||||||
const replyingToEvent = event().replyingToEvent();
|
const replyingToEvent = event().replyingToEvent();
|
||||||
@@ -141,6 +151,12 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (contentRef != null) {
|
||||||
|
setOverflow(contentRef.scrollHeight > contentRef.clientHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="nostr-textnote flex flex-col">
|
<div class="nostr-textnote flex flex-col">
|
||||||
<div class="flex w-full gap-1">
|
<div class="flex w-full gap-1">
|
||||||
@@ -177,33 +193,49 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
<div class="created-at shrink-0">{createdAt()}</div>
|
<div class="created-at shrink-0">{createdAt()}</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={showReplyEvent()} keyed>
|
<div
|
||||||
{(id) => (
|
ref={contentRef}
|
||||||
<div class="mt-1 rounded border p-1">
|
class="overflow-hidden"
|
||||||
<TextNoteDisplayById eventId={id} actions={false} embedding={false} />
|
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>
|
</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>
|
||||||
<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()}>
|
<Show when={actions()}>
|
||||||
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Copy: Component<CopyProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
<Show when={showPopup()}>
|
<Show when={showPopup()}>
|
||||||
<div
|
<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"
|
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
||||||
>
|
>
|
||||||
Copied!
|
Copied!
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
|||||||
// nrelay and naddr is not supported by nostr-tools
|
// nrelay and naddr is not supported by nostr-tools
|
||||||
...event.content.matchAll(/(?<nip19>(npub|note|nprofile|nevent)1[ac-hj-np-z02-9]+)/gi),
|
...event.content.matchAll(/(?<nip19>(npub|note|nprofile|nevent)1[ac-hj-np-z02-9]+)/gi),
|
||||||
...event.content.matchAll(
|
...event.content.matchAll(
|
||||||
/(?<url>(https?|wss?):\/\/[-a-zA-Z0-9.]+(?:\/[-\w.@%:]+|\/)*(?:\?[-\w=.@%:&]*)?(?:#[-\w=.%:&]*)?)/g,
|
/(?<url>(https?|wss?):\/\/[-a-zA-Z0-9.]+(?:\/[-\w.@%:]+|\/)*(?:\?[-\w=.@%:&]+)?(?:#[-\w=.%:&]+)?)/g,
|
||||||
),
|
),
|
||||||
].sort((a, b) => (a.index as number) - (b.index as number));
|
].sort((a, b) => (a.index as number) - (b.index as number));
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|||||||
@@ -479,5 +479,3 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U
|
|||||||
|
|
||||||
return { followings, followingPubkeys, query };
|
return { followings, followingPubkeys, query };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useFollowings;
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { getEventHash, type Event as NostrEvent, type Pub } from 'nostr-tools';
|
import {
|
||||||
|
getEventHash,
|
||||||
|
type UnsignedEvent,
|
||||||
|
type Event as NostrEvent,
|
||||||
|
type Pub,
|
||||||
|
type Kind,
|
||||||
|
} from 'nostr-tools';
|
||||||
|
|
||||||
import '@/types/nostr.d';
|
import '@/types/nostr.d';
|
||||||
import usePool from '@/nostr/usePool';
|
import usePool from '@/nostr/usePool';
|
||||||
|
|
||||||
const currentDate = (): number => Math.floor(Date.now() / 1000);
|
import epoch from '@/utils/epoch';
|
||||||
|
|
||||||
// NIP-20: Command Result
|
// NIP-20: Command Result
|
||||||
const waitCommandResult = (pub: Pub, relayUrl: string): Promise<void> => {
|
const waitCommandResult = (pub: Pub, relayUrl: string): Promise<void> => {
|
||||||
@@ -22,8 +28,11 @@ const waitCommandResult = (pub: Pub, relayUrl: string): Promise<void> => {
|
|||||||
const useCommands = () => {
|
const useCommands = () => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
|
|
||||||
const publishEvent = async (relayUrls: string[], event: NostrEvent): Promise<Promise<void>[]> => {
|
const publishEvent = async (
|
||||||
const preSignedEvent: NostrEvent = { ...event };
|
relayUrls: string[],
|
||||||
|
event: UnsignedEvent,
|
||||||
|
): Promise<Promise<void>[]> => {
|
||||||
|
const preSignedEvent: UnsignedEvent = { ...event };
|
||||||
preSignedEvent.id = getEventHash(preSignedEvent);
|
preSignedEvent.id = getEventHash(preSignedEvent);
|
||||||
|
|
||||||
if (window.nostr == null) {
|
if (window.nostr == null) {
|
||||||
@@ -75,10 +84,10 @@ const useCommands = () => {
|
|||||||
|
|
||||||
const mergedTags = [...eTags, ...pTags, ...additionalTags];
|
const mergedTags = [...eTags, ...pTags, ...additionalTags];
|
||||||
|
|
||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: UnsignedEvent = {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: currentDate(),
|
created_at: epoch(),
|
||||||
tags: mergedTags,
|
tags: mergedTags,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
@@ -102,17 +111,16 @@ const useCommands = () => {
|
|||||||
notifyPubkey: string;
|
notifyPubkey: string;
|
||||||
}): Promise<Promise<void>[]> {
|
}): Promise<Promise<void>[]> {
|
||||||
// TODO ensure that content is + or - or emoji.
|
// TODO ensure that content is + or - or emoji.
|
||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: UnsignedEvent = {
|
||||||
kind: 7,
|
kind: 7,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: currentDate(),
|
created_at: epoch(),
|
||||||
tags: [
|
tags: [
|
||||||
['e', eventId, ''],
|
['e', eventId, ''],
|
||||||
['p', notifyPubkey],
|
['p', notifyPubkey],
|
||||||
],
|
],
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
console.log(preSignedEvent);
|
|
||||||
return publishEvent(relayUrls, preSignedEvent);
|
return publishEvent(relayUrls, preSignedEvent);
|
||||||
},
|
},
|
||||||
// NIP-18
|
// NIP-18
|
||||||
@@ -127,10 +135,10 @@ const useCommands = () => {
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
notifyPubkey: string;
|
notifyPubkey: string;
|
||||||
}): Promise<Promise<void>[]> {
|
}): Promise<Promise<void>[]> {
|
||||||
const preSignedEvent: NostrEvent = {
|
const preSignedEvent: UnsignedEvent = {
|
||||||
kind: 6,
|
kind: 6 as Kind,
|
||||||
pubkey,
|
pubkey,
|
||||||
created_at: currentDate(),
|
created_at: epoch(),
|
||||||
tags: [
|
tags: [
|
||||||
['e', eventId, ''],
|
['e', eventId, ''],
|
||||||
['p', notifyPubkey],
|
['p', notifyPubkey],
|
||||||
|
|||||||
26
src/nostr/useFollowers.ts
Normal file
26
src/nostr/useFollowers.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createMemo, createSignal } from 'solid-js';
|
||||||
|
import { Kind } from 'nostr-tools';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
|
import useConfig from '@/nostr/useConfig';
|
||||||
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
|
export type UseFollowersProps = {
|
||||||
|
pubkey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useFollowers(propsProvider: () => UseFollowersProps) {
|
||||||
|
const { config } = useConfig();
|
||||||
|
const props = createMemo(propsProvider);
|
||||||
|
|
||||||
|
const { events } = useSubscription(() => ({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
filters: [{ kinds: [Kind.Contacts], '#p': [props().pubkey] }],
|
||||||
|
limit: Infinity,
|
||||||
|
continuous: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const followersPubkeys = () => uniq(events()?.map((ev) => ev.pubkey));
|
||||||
|
|
||||||
|
return { followersPubkeys };
|
||||||
|
}
|
||||||
@@ -7,10 +7,16 @@ export type UseSubscriptionProps = {
|
|||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
options?: SubscriptionOptions;
|
options?: SubscriptionOptions;
|
||||||
// subscribe not only stored events but also new events published after the subscription
|
/**
|
||||||
// default is true
|
* subscribe not only stored events but also new events published after the subscription
|
||||||
clientEventFilter?: (event: NostrEvent) => boolean;
|
* default is true
|
||||||
|
*/
|
||||||
continuous?: boolean;
|
continuous?: boolean;
|
||||||
|
/**
|
||||||
|
* limit the number of events
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
clientEventFilter?: (event: NostrEvent) => boolean;
|
||||||
onEvent?: (event: NostrEvent & { id: string }) => void;
|
onEvent?: (event: NostrEvent & { id: string }) => void;
|
||||||
onEOSE?: () => void;
|
onEOSE?: () => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
@@ -32,6 +38,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
if (props == null) return;
|
if (props == null) return;
|
||||||
|
|
||||||
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
||||||
|
const limit = props.limit ?? 50;
|
||||||
|
|
||||||
const sub = pool().sub(relayUrls, filters, options);
|
const sub = pool().sub(relayUrls, filters, options);
|
||||||
let subscribing = true;
|
let subscribing = true;
|
||||||
@@ -53,8 +60,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
storedEvents.push(event);
|
storedEvents.push(event);
|
||||||
} else {
|
} else {
|
||||||
setEvents((current) => {
|
setEvents((current) => {
|
||||||
// いったん50件だけ保持
|
const sorted = sortEvents([event, ...current].slice(0, limit));
|
||||||
const sorted = sortEvents([event, ...current].slice(0, 50));
|
|
||||||
// FIXME なぜか重複して取得される問題があるが一旦uniqByで対処
|
// FIXME なぜか重複して取得される問題があるが一旦uniqByで対処
|
||||||
// https://github.com/syusui-s/rabbit/issues/5
|
// https://github.com/syusui-s/rabbit/issues/5
|
||||||
const deduped = uniqBy(sorted, (e) => e.id);
|
const deduped = uniqBy(sorted, (e) => e.id);
|
||||||
|
|||||||
35
src/nostr/useVerification.ts
Normal file
35
src/nostr/useVerification.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createMemo, type Accessor } from 'solid-js';
|
||||||
|
import { createQuery, type CreateQueryResult } from '@tanstack/solid-query';
|
||||||
|
import { nip05, nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
export type UseVerificationProps = {
|
||||||
|
nip05: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseVerification = {
|
||||||
|
verification: Accessor<nip19.ProfilePointer | null>;
|
||||||
|
query: CreateQueryResult<nip19.ProfilePointer | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useVerification = (propsProvider: () => UseVerificationProps | null): UseVerification => {
|
||||||
|
const props = createMemo(propsProvider);
|
||||||
|
const query = createQuery(
|
||||||
|
() => ['useVerification', props()] as const,
|
||||||
|
({ queryKey, signal }) => {
|
||||||
|
const [, currentProps] = queryKey;
|
||||||
|
if (currentProps == null) return Promise.resolve(null);
|
||||||
|
const { nip05: nip05string } = currentProps;
|
||||||
|
return nip05.queryProfile(nip05string);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staleTime: 30 * 60 * 1000, // 30 min
|
||||||
|
cacheTime: 24 * 60 * 60 * 1000, // 24 hour
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const verification = () => query?.data ?? null;
|
||||||
|
|
||||||
|
return { verification, query };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useVerification;
|
||||||
@@ -15,6 +15,7 @@ import Column from '@/components/Column';
|
|||||||
import SideBar from '@/components/SideBar';
|
import SideBar from '@/components/SideBar';
|
||||||
import Timeline from '@/components/Timeline';
|
import Timeline from '@/components/Timeline';
|
||||||
import Notification from '@/components/Notification';
|
import Notification from '@/components/Notification';
|
||||||
|
import ProfileDisplay from '@/components/ProfileDisplay';
|
||||||
|
|
||||||
import usePool from '@/nostr/usePool';
|
import usePool from '@/nostr/usePool';
|
||||||
import useConfig from '@/nostr/useConfig';
|
import useConfig from '@/nostr/useConfig';
|
||||||
@@ -24,10 +25,11 @@ import usePubkey from '@/nostr/usePubkey';
|
|||||||
|
|
||||||
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
|
import { useMountShortcutKeys } from '@/hooks/useShortcutKeys';
|
||||||
import usePersistStatus from '@/hooks/usePersistStatus';
|
import usePersistStatus from '@/hooks/usePersistStatus';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
|
||||||
import ProfileDisplay from '@/components/ProfileDisplay';
|
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
|
|
||||||
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
|
import epoch from '@/utils/epoch';
|
||||||
|
|
||||||
const Home: Component = () => {
|
const Home: Component = () => {
|
||||||
useMountShortcutKeys();
|
useMountShortcutKeys();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -118,7 +120,7 @@ const Home: Component = () => {
|
|||||||
{
|
{
|
||||||
kinds: [1, 6],
|
kinds: [1, 6],
|
||||||
limit: 25,
|
limit: 25,
|
||||||
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
|
since: epoch() - 12 * 60 * 60,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
@@ -131,7 +133,7 @@ const Home: Component = () => {
|
|||||||
kinds: [1],
|
kinds: [1],
|
||||||
search: '#nostrstudy',
|
search: '#nostrstudy',
|
||||||
limit: 25,
|
limit: 25,
|
||||||
since: Math.floor(Date.now() / 1000) - 12 * 60 * 60,
|
since: epoch() - 12 * 60 * 60,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|||||||
4
src/types/nostr.d.ts
vendored
4
src/types/nostr.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
// The original code was published under the public domain license (CC0-1.0).
|
// The original code was published under the public domain license (CC0-1.0).
|
||||||
// https://gist.github.com/syusui-s/cd5482ddfc83792b54a756759acbda55
|
// https://gist.github.com/syusui-s/cd5482ddfc83792b54a756759acbda55
|
||||||
import { type Event as NostrEvent } from 'nostr-tools';
|
import { type UnsignedEvent, type Event as NostrEvent } from 'nostr-tools';
|
||||||
|
|
||||||
type NostrAPI = {
|
type NostrAPI = {
|
||||||
/** returns a public key as hex */
|
/** returns a public key as hex */
|
||||||
getPublicKey(): Promise<string>;
|
getPublicKey(): Promise<string>;
|
||||||
/** takes an event object, adds `id`, `pubkey` and `sig` and returns it */
|
/** takes an event object, adds `id`, `pubkey` and `sig` and returns it */
|
||||||
signEvent(event: NostrEvent): Promise<NostrEvent>;
|
signEvent(event: UnsignedEvent): Promise<NostrEvent>;
|
||||||
|
|
||||||
// Optional
|
// Optional
|
||||||
|
|
||||||
|
|||||||
3
src/utils/epoch.ts
Normal file
3
src/utils/epoch.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const epoch = (): number => Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
export default epoch;
|
||||||
Reference in New Issue
Block a user