mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
update
This commit is contained in:
@@ -5,9 +5,7 @@ type ColumnItemProps = {
|
||||
};
|
||||
|
||||
const ColumnItem: Component<ColumnItemProps> = (props) => {
|
||||
return (
|
||||
<div class="flex w-full flex-row gap-1 overflow-hidden border-b p-1">{props.children}</div>
|
||||
);
|
||||
return <div class="overflow-hidden border-b p-1">{props.children}</div>;
|
||||
};
|
||||
|
||||
export default ColumnItem;
|
||||
|
||||
@@ -7,7 +7,7 @@ import useConfig from '@/clients/useConfig';
|
||||
import useEvent from '@/clients/useEvent';
|
||||
import useProfile from '@/clients/useProfile';
|
||||
|
||||
import UserNameDisplay from '@/components/UserNameDisplay';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import TextNote from '@/components/TextNote';
|
||||
|
||||
export type DeprecatedRepostProps = {
|
||||
@@ -32,7 +32,7 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
|
||||
<ArrowPathRoundedSquare />
|
||||
</div>
|
||||
<div class="truncate break-all">
|
||||
<UserNameDisplay pubkey={props.event.pubkey} />
|
||||
<UserDisplayName pubkey={props.event.pubkey} />
|
||||
{' Reposted'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div class="p-1">
|
||||
<form class="grid w-64 gap-1" onSubmit={handleSubmit}>
|
||||
<form class="flex flex-col gap-1" onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
name="text"
|
||||
class="rounded border-none"
|
||||
|
||||
@@ -1,6 +1,62 @@
|
||||
import { type Component } from 'solid-js';
|
||||
import { createSignal, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||
|
||||
const ReplyPostForm = () => {
|
||||
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
|
||||
|
||||
type ReplyPostFormProps = {
|
||||
replyTo: NostrEvent;
|
||||
onPost: (textNote: { content: string }) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ReplyPostForm: Component<ReplyPostFormProps> = (props: ReplyPostFormProps) => {
|
||||
const [text, setText] = createSignal<string>('');
|
||||
|
||||
const clearText = () => setText('');
|
||||
|
||||
const handleChangeText: JSX.EventHandler<HTMLTextAreaElement, Event> = (ev) => {
|
||||
setText(ev.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
|
||||
ev.preventDefault();
|
||||
// TODO 投稿完了したかどうかの検知をしたい
|
||||
props.onPost({ content: text() });
|
||||
clearText();
|
||||
};
|
||||
|
||||
const submitDisabled = createMemo(() => text().trim().length === 0);
|
||||
|
||||
return (
|
||||
<div class="p-1">
|
||||
<div>
|
||||
{'Replying to '}
|
||||
{props.replyTo.pubkey}
|
||||
</div>
|
||||
<form class="grid w-full gap-1" onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
name="text"
|
||||
class="rounded border-none"
|
||||
rows={4}
|
||||
placeholder="返信を投稿"
|
||||
onInput={handleChangeText}
|
||||
value={text()}
|
||||
/>
|
||||
<div class="flex justify-between">
|
||||
{/* TODO あとでちゃんとアイコンにする */}
|
||||
<button onClick={() => props.onClose()}>X</button>
|
||||
<button
|
||||
class="h-7 w-7 rounded bg-primary p-2 font-bold text-white"
|
||||
classList={{ 'bg-primary-disabled': submitDisabled(), 'bg-primary': !submitDisabled() }}
|
||||
type="submit"
|
||||
disabled={submitDisabled()}
|
||||
>
|
||||
<PaperAirplane />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplyPostForm;
|
||||
|
||||
@@ -12,7 +12,7 @@ const SideBar: Component<SideBarProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div class="flex shrink-0 flex-row border-r bg-sidebar-bg">
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r py-5">
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r border-rose-200 py-5">
|
||||
<button
|
||||
class={`h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl font-bold text-white`}
|
||||
onClick={() => setFormOpened((current) => !current)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Show, For, createMemo, type JSX, type Component } from 'solid-js';
|
||||
import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js';
|
||||
import type { Event as NostrEvent } from 'nostr-tools/event';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
||||
import HeartSolid from 'heroicons/24/solid/heart.svg';
|
||||
@@ -18,6 +19,7 @@ import { formatRelative } from '@/utils/formatDate';
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
||||
import ReplyPostForm from '@/components/ReplyPostForm';
|
||||
|
||||
export type TextNoteProps = {
|
||||
event: NostrEvent;
|
||||
@@ -28,6 +30,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
const [config] = useConfig();
|
||||
const commands = useCommands();
|
||||
const pubkey = usePubkey();
|
||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||
|
||||
const { profile: author } = useProfile(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
@@ -48,11 +51,24 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
const isRepostedByMe = createMemo(() => isRepostedBy(pubkey()));
|
||||
|
||||
const replyingToPubKeys = createMemo(() =>
|
||||
props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]),
|
||||
uniq(props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1])),
|
||||
);
|
||||
// TODO 日付をいい感じにフォーマットする関数を作る
|
||||
const createdAt = () => formatRelative(new Date(props.event.created_at * 1000), currentDate());
|
||||
|
||||
const handleReplyPost = ({ content }: { content: string }) => {
|
||||
commands
|
||||
.publishTextNote({
|
||||
relayUrls: config().relayUrls,
|
||||
pubkey: pubkey(),
|
||||
content,
|
||||
notifyPubkeys: [props.event.pubkey, ...replyingToPubKeys()],
|
||||
replyEventId: props.event.id,
|
||||
})
|
||||
.then(() => {
|
||||
setShowReplyForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -87,85 +103,99 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
return (
|
||||
<div class="textnote">
|
||||
<ColumnItem>
|
||||
<div class="author-icon h-10 w-10 shrink-0">
|
||||
<Show when={author()?.picture}>
|
||||
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
||||
<img
|
||||
src={author()?.picture}
|
||||
alt="icon"
|
||||
// TODO autofit
|
||||
class="h-10 w-10 rounded"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<div class="author flex min-w-0 truncate">
|
||||
{/* TODO link to author */}
|
||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="author-name truncate pr-1 font-bold">{author()?.display_name}</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex w-full gap-1">
|
||||
<div class="author-icon h-10 w-10 shrink-0">
|
||||
<Show when={author()?.picture}>
|
||||
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
||||
<img
|
||||
src={author()?.picture}
|
||||
alt="icon"
|
||||
// TODO autofit
|
||||
class="h-10 w-10 rounded"
|
||||
/>
|
||||
</Show>
|
||||
<div class="author-username truncate text-zinc-600">
|
||||
<Show when={author()?.name} fallback={props.event.pubkey}>
|
||||
@{author()?.name}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="min-w-0 flex-auto">
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<div class="author flex min-w-0 truncate">
|
||||
{/* TODO link to author */}
|
||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="author-name truncate pr-1 font-bold">{author()?.display_name}</div>
|
||||
</Show>
|
||||
<div class="author-username truncate text-zinc-600">
|
||||
<Show when={author()?.name} fallback={props.event.pubkey}>
|
||||
@{author()?.name}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="created-at shrink-0">{createdAt()}</div>
|
||||
</div>
|
||||
<Show when={replyingToPubKeys().length > 0}>
|
||||
<div class="text-xs">
|
||||
{'Replying to '}
|
||||
<For each={replyingToPubKeys()}>
|
||||
{(replyToPubkey: string) => (
|
||||
<span class="pr-1 text-blue-500 underline">
|
||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="content whitespace-pre-wrap break-all">
|
||||
<TextNoteContentDisplay event={props.event} embedding={true} />
|
||||
</div>
|
||||
<div class="flex w-48 items-center justify-between gap-8 pt-1">
|
||||
<button
|
||||
class="h-4 w-4 text-zinc-400"
|
||||
onClick={() => setShowReplyForm((current) => !current)}
|
||||
>
|
||||
<ChatBubbleLeft />
|
||||
</button>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isRepostedByMe(),
|
||||
'text-green-400': isRepostedByMe(),
|
||||
}}
|
||||
>
|
||||
<button class="h-4 w-4" onClick={handleReaction}>
|
||||
<ArrowPathRoundedSquare />
|
||||
</button>
|
||||
<Show when={reposts().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reposts().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMe(),
|
||||
'text-rose-400': isReactedByMe(),
|
||||
}}
|
||||
>
|
||||
<button class="h-4 w-4" onClick={handleReaction}>
|
||||
<Show when={isReactedByMe()} fallback={<HeartOutlined />}>
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={reactions().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reactions().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="created-at shrink-0">{createdAt()}</div>
|
||||
</div>
|
||||
<Show when={replyingToPubKeys().length > 0}>
|
||||
<div class="text-xs">
|
||||
{'Replying to '}
|
||||
<For each={replyingToPubKeys()}>
|
||||
{(replyToPubkey: string) => (
|
||||
<span class="pr-1 text-blue-500 underline">
|
||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={showReplyForm()}>
|
||||
<ReplyPostForm
|
||||
replyTo={props.event}
|
||||
onPost={handleReplyPost}
|
||||
onClose={() => setShowReplyForm(false)}
|
||||
/>
|
||||
</Show>
|
||||
<div class="content whitespace-pre-wrap break-all">
|
||||
<TextNoteContentDisplay event={props.event} embedding={true} />
|
||||
</div>
|
||||
<div class="flex w-48 items-center justify-between gap-8 pt-1">
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<ChatBubbleLeft />
|
||||
</button>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isRepostedByMe(),
|
||||
'text-green-400': isRepostedByMe(),
|
||||
}}
|
||||
>
|
||||
<button class="h-4 w-4" onClick={handleReaction}>
|
||||
<ArrowPathRoundedSquare />
|
||||
</button>
|
||||
<Show when={reposts().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reposts().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMe(),
|
||||
'text-rose-400': isReactedByMe(),
|
||||
}}
|
||||
>
|
||||
<button class="h-4 w-4" onClick={handleReaction}>
|
||||
<Show when={isReactedByMe()} fallback={<HeartOutlined />}>
|
||||
<HeartSolid />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={reactions().length > 0}>
|
||||
<div class="text-sm text-zinc-400">{reactions().length}</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button class="h-4 w-4 text-zinc-400">
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ColumnItem>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Switch, Match, type Component, Show } from 'solid-js';
|
||||
import { type Event as NostrEvent } from 'nostr-tools/event';
|
||||
import HeartSolid from 'heroicons/24/solid/heart.svg';
|
||||
|
||||
import UserNameDisplay from '@/components/UserNameDisplay';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import TextNote from '@/components/TextNote';
|
||||
|
||||
import useConfig from '@/clients/useConfig';
|
||||
@@ -54,7 +54,7 @@ const Reaction: Component<ReactionProps> = (props) => {
|
||||
</div>
|
||||
<div>
|
||||
<span class="truncate whitespace-pre-wrap break-all font-bold">
|
||||
<UserNameDisplay pubkey={props.event.pubkey} />
|
||||
<UserDisplayName pubkey={props.event.pubkey} />
|
||||
</span>
|
||||
{' reacted'}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user