mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 06:24:25 +01:00
update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
45
src/components/event/ZapReceipt.tsx
Normal file
45
src/components/event/ZapReceipt.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
28
src/components/modal/EventDebugModal.tsx
Normal file
28
src/components/modal/EventDebugModal.tsx
Normal 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;
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ type UseHandleCommandProps = {
|
||||
|
||||
type CommandBase<T> = { command: T };
|
||||
|
||||
export type OpenPostForm = CommandBase<'openPostForm'>;
|
||||
export type OpenPostForm = CommandBase<'openPostForm'> & { content?: string };
|
||||
export type ClosePostForm = CommandBase<'closePostForm'>;
|
||||
export type MoveToNextItem = CommandBase<'moveToNextItem'>;
|
||||
export type MoveToPrevItem = CommandBase<'moveToPrevItem'>;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createSignal, onMount } from 'solid-js';
|
||||
|
||||
// TODO Find a better way to solve this. Firefox on Windows can cause 2px gap.
|
||||
const Offset = 2;
|
||||
|
||||
const useDetectOverflow = () => {
|
||||
let elementRef: HTMLElement | undefined;
|
||||
const [overflow, setOverflow] = createSignal(false);
|
||||
@@ -10,7 +13,7 @@ const useDetectOverflow = () => {
|
||||
|
||||
onMount(() => {
|
||||
if (elementRef != null) {
|
||||
setOverflow(elementRef.scrollHeight > elementRef.clientHeight);
|
||||
setOverflow(elementRef.scrollHeight > elementRef.clientHeight + Offset);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export type UseFollowings = {
|
||||
query: CreateQueryResult<NostrEvent | null>;
|
||||
};
|
||||
|
||||
export const useFollowings = (propsProvider: () => UseFollowingsProps | null): UseFollowings => {
|
||||
const useFollowings = (propsProvider: () => UseFollowingsProps | null): UseFollowings => {
|
||||
const queryClient = useQueryClient();
|
||||
const props = createMemo(propsProvider);
|
||||
const genQueryKey = () => ['useFollowings', props()] as const;
|
||||
|
||||
Reference in New Issue
Block a user