This commit is contained in:
Shusui MOYATANI
2023-06-17 01:42:04 +09:00
parent df8bc01e92
commit 6ba622328b
14 changed files with 159 additions and 47 deletions

View File

@@ -2,14 +2,15 @@ import { Component, JSX, Show, createSignal } from 'solid-js';
import useDetectOverflow from '@/hooks/useDetectOverflow';
import useFormatDate from '@/hooks/useFormatDate';
import useProfile from '@/nostr/useProfile';
import npubEncodeFallback from '@/utils/npubEncodeFallback';
export type PostProps = {
author: JSX.Element;
authorPubkey: string;
createdAt: Date;
content: JSX.Element;
actions?: JSX.Element;
footer?: JSX.Element;
authorPictureUrl?: string;
onShowProfile?: () => void;
onShowEvent?: () => void;
};
@@ -21,6 +22,10 @@ const Post: Component<PostProps> = (props) => {
const [showOverflow, setShowOverflow] = createSignal();
const createdAt = () => formatDate(props.createdAt);
const { profile: author } = useProfile(() => ({
pubkey: props.authorPubkey,
}));
return (
<div class="post flex flex-col">
<div class="flex w-full gap-1">
@@ -32,7 +37,7 @@ const Post: Component<PostProps> = (props) => {
props.onShowProfile?.();
}}
>
<Show when={props.authorPictureUrl} keyed>
<Show when={author()?.picture} keyed>
{(url) => <img src={url} alt="icon" class="h-full w-full rounded object-cover" />}
</Show>
</button>
@@ -46,7 +51,22 @@ const Post: Component<PostProps> = (props) => {
props?.onShowProfile?.();
}}
>
{props.author}
<span class="author flex min-w-0 truncate hover:text-blue-500">
<Show when={(author()?.display_name?.length ?? 0) > 0}>
<div class="author-name truncate pr-1 font-bold hover:underline">
{author()?.display_name}
</div>
</Show>
<div class="author-username truncate text-zinc-600">
<Show
when={author()?.name != null}
fallback={`@${npubEncodeFallback(props.authorPubkey)}`}
>
@{author()?.name}
</Show>
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
</div>
</span>
</button>
<div class="created-at shrink-0">
<button

View File

@@ -76,7 +76,7 @@ const Column: Component<ColumnProps> = (props) => {
<div></div>
</button>
</div>
<ul class="scrollbar flex h-full flex-col overflow-y-scroll scroll-smooth">
<ul class="scrollbar flex h-full flex-col overflow-y-scroll scroll-smooth pb-8">
<TimelineContentDisplay timelineContent={timeline} />
</ul>
</div>

View File

@@ -38,8 +38,8 @@ const Reaction: Component<ReactionProps> = (props) => {
// if the reacted event is not found, it should be a removed event
<Show when={!isRemoved() || shouldMuteEvent(props.event)}>
<div class="flex gap-1 px-1 text-sm">
<div class="notification-icon flex place-items-center">
<Switch fallback={props.event.content}>
<div class="notification-icon flex max-w-[64px] place-items-center">
<Switch fallback={<span class="truncate">{props.event.content}</span>}>
<Match when={props.event.content === '+'}>
<span class="h-4 w-4 pt-[1px] text-rose-400">
<HeartSolid />

View File

@@ -0,0 +1,45 @@
import { Component, Show, createMemo } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools';
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
import UserNameDisplay from '@/components/UserDisplayName';
import useConfig from '@/core/useConfig';
import { genericEvent } from '@/nostr/event';
export type ZapReceiptProps = {
event: NostrEvent;
};
const ZapReceipt: Component<ZapReceiptProps> = (props) => {
const { shouldMuteEvent } = useConfig();
const event = createMemo(() => genericEvent(props.event));
const zapRequest = () => {
const description = event().findFirstTagByName('description');
if (description == null) return null;
try {
// TODO verify that this is event
return JSON.parse(description[1]) as NostrEvent;
} catch (err) {
console.error('failed to parse zap receipt', description);
return null;
}
};
const amount = () => {
return event().findFirstTagByName('amount');
};
return (
<Show when={!shouldMuteEvent(props.event)}>
<UserNameDisplay pubkey={zapRequest().pubkey} />
<pre>{JSON.stringify(props.event, null, 2)}</pre>
</Show>
);
};
export default ZapReceipt;

View File

@@ -16,6 +16,7 @@ import EventDisplayById from '@/components/event/EventDisplayById';
import ContentWarningDisplay from '@/components/event/textNote/ContentWarningDisplay';
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
import EventDebugModal from '@/components/modal/EventDebugModal';
import NotePostForm from '@/components/NotePostForm';
import Post from '@/components/Post';
import { useTimelineContext } from '@/components/timeline/TimelineContext';
@@ -23,12 +24,10 @@ import useConfig from '@/core/useConfig';
import useModalState from '@/hooks/useModalState';
import { textNote } from '@/nostr/event';
import useCommands from '@/nostr/useCommands';
import useProfile from '@/nostr/useProfile';
import usePubkey from '@/nostr/usePubkey';
import useReactions from '@/nostr/useReactions';
import useReposts from '@/nostr/useReposts';
import ensureNonNull from '@/utils/ensureNonNull';
import npubEncodeFallback from '@/utils/npubEncodeFallback';
import timeout from '@/utils/timeout';
export type TextNoteDisplayProps = {
@@ -49,7 +48,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
const pubkey = usePubkey();
return (
<div class="flex gap-2 py-1">
<div class="flex gap-2 overflow-x-auto py-1">
<For each={[...props.reactionsGroupedByContent.entries()]}>
{([content, events]) => {
const isReactedByMeWithThisContent =
@@ -57,7 +56,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
return (
<button
class="flex h-6 items-center rounded border px-1"
class="flex h-6 max-w-[128px] items-center rounded border px-1"
classList={{
'text-zinc-400': !isReactedByMeWithThisContent,
'hover:bg-zinc-50': !isReactedByMeWithThisContent,
@@ -68,7 +67,10 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
type="button"
onClick={() => props.onReaction(content)}
>
<Show when={content === '+'} fallback={<span class="text-base">{content}</span>}>
<Show
when={content === '+'}
fallback={<span class="truncate text-base">{content}</span>}
>
<span class="inline-block h-3 w-3 pt-[1px] text-rose-400">
<HeartSolid />
</span>
@@ -93,6 +95,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const [reacted, setReacted] = createSignal(false);
const [reposted, setReposted] = createSignal(false);
const [showReplyForm, setShowReplyForm] = createSignal(false);
const [showEventDebug, setShowEventDebug] = createSignal(false);
const closeReplyForm = () => setShowReplyForm(false);
const event = createMemo(() => textNote(props.event));
@@ -100,10 +103,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const embedding = () => props.embedding ?? true;
const actions = () => props.actions ?? true;
const { profile: author } = useProfile(() => ({
pubkey: props.event.pubkey,
}));
const {
reactions,
reactionsGroupedByContent,
@@ -189,11 +188,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
},
},
{
content: () => 'JSONとしてコピー',
content: () => 'JSONを確認',
onSelect: () => {
navigator.clipboard
.writeText(JSON.stringify(props.event, null, 2))
.catch((err) => window.alert(err));
setShowEventDebug(true);
},
},
{
@@ -292,25 +289,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
return (
<div class="nostr-textnote">
<Post
author={
<span class="author flex min-w-0 truncate hover:text-blue-500">
<Show when={(author()?.display_name?.length ?? 0) > 0}>
<div class="author-name truncate pr-1 font-bold hover:underline">
{author()?.display_name}
</div>
</Show>
<div class="author-username truncate text-zinc-600">
<Show
when={author()?.name != null}
fallback={`@${npubEncodeFallback(event().pubkey)}`}
>
@{author()?.name}
</Show>
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
</div>
</span>
}
authorPictureUrl={author()?.picture}
authorPubkey={event().pubkey}
createdAt={event().createdAtAsDate()}
content={
<div class="textnote-content">
@@ -458,6 +437,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
timelineContext?.setTimeline({ type: 'Replies', event: props.event });
}}
/>
<Show when={showEventDebug()}>
<EventDebugModal event={props.event} onClose={() => setShowEventDebug(false)} />
</Show>
</div>
);
};

View File

@@ -0,0 +1,28 @@
import { Component, For, createMemo } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools';
import BasicModal from '@/components/modal/BasicModal';
import Copy from '@/components/utils/Copy';
export type EventDebugModalProps = {
event: NostrEvent;
onClose: () => void;
};
const EventDebugModal: Component<EventDebugModalProps> = (props) => {
const json = createMemo(() => JSON.stringify(props.event, null, 2));
return (
<BasicModal onClose={props.onClose}>
<div class="p-2">
<pre class="whitespace-pre-wrap break-all rounded border p-4 text-xs">{json()}</pre>
<div class="flex justify-end">
<Copy class="h-4 w-4" text={json()} />
</div>
</div>
</BasicModal>
);
};
export default EventDebugModal;

View File

@@ -77,6 +77,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
})),
);
const following = () => myFollowingPubkeys().includes(props.pubkey);
const refetchMyFollowing = () => myFollowingQuery.refetch();
const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings(
() => ({ pubkey: props.pubkey }),
@@ -116,35 +117,58 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
},
});
const follow = () => {
const handlePromise =
<Params extends any[], T>(f: (...params: Params) => Promise<T>) =>
(onError: (err: any) => void) =>
(...params: Params) => {
f(...params).catch((err) => {
onError(err);
});
};
const follow = handlePromise(async () => {
const p = myPubkey();
if (p == null) return;
if (!myFollowingQuery.isFetched) return;
await refetchMyFollowing();
updateContactsMutation.mutate({
relayUrls: config().relayUrls,
pubkey: p,
content: myFollowingQuery.data?.content ?? '',
followingPubkeys: uniq([...myFollowingPubkeys(), props.pubkey]),
});
};
})((err) => {
console.log('failed to follow', err);
});
const unfollow = () => {
const unfollow = handlePromise(async () => {
const p = myPubkey();
if (p == null) return;
if (!myFollowingQuery.isFetched) return;
if (!window.confirm('本当にフォロー解除しますか?')) return;
await refetchMyFollowing();
updateContactsMutation.mutate({
relayUrls: config().relayUrls,
pubkey: p,
content: myFollowingQuery.data?.content ?? '',
followingPubkeys: myFollowingPubkeys().filter((k) => k !== props.pubkey),
});
};
})((err) => {
console.log('failed to unfollow', err);
});
const menu: MenuItem[] = [
/*
{
content: () => 'ユーザ宛に投稿',
onSelect: () => {
navigator.clipboard.writeText(npub()).catch((err) => window.alert(err));
},
},
*/
{
content: () => 'IDをコピー',
onSelect: () => {

View File

@@ -76,6 +76,8 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
const loading = () => query.isLoading || mutation.isLoading;
const disabled = () => loading();
setInterval(() => console.log(query.isLoading, mutation.isLoading), 1000);
const otherProperties = () =>
omit(profile(), [
'picture',

View File

@@ -6,6 +6,7 @@ import ColumnItem from '@/components/ColumnItem';
import Reaction from '@/components/event/Reaction';
import Repost from '@/components/event/Repost';
import TextNote from '@/components/event/TextNote';
import ZapReceipt from '@/components/event/ZapReceipt';
import useConfig from '@/core/useConfig';
export type NotificationProps = {
@@ -36,6 +37,13 @@ const Notification: Component<NotificationProps> = (props) => {
<Repost event={event} />
</ColumnItem>
</Match>
{/*
<Match when={event.kind === Kind.Zap}>
<ColumnItem>
<ZapReceipt event={event} />
</ColumnItem>
</Match>
*/}
</Switch>
</Show>
)}

View File

@@ -1,4 +1,4 @@
import { createSignal, Show, type Component } from 'solid-js';
import { createSignal, Show, type Component, type JSX } from 'solid-js';
import ClipboardDocument from 'heroicons/24/outline/clipboard-document.svg';