This commit is contained in:
Shusui MOYATANI
2023-02-25 23:35:58 +09:00
parent 717c264c2f
commit 57bc321436
27 changed files with 563 additions and 157 deletions

View File

@@ -18,10 +18,6 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
const pubkey = () => props.event.pubkey;
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
if (eventId() == null) {
return 'event not found';
}
const { profile } = useProfile(() => ({ relayUrls: config().relayUrls, pubkey: pubkey() }));
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
@@ -33,7 +29,7 @@ const DeprecatedRepost: Component<DeprecatedRepostProps> = (props) => {
</div>
<div>{profile()?.display_name} Reposted</div>
</div>
<Show when={event() != null}>
<Show when={event() != null} fallback={'loading'}>
<TextNote event={event()} />
</Show>
</div>

View File

@@ -1,4 +1,4 @@
import { createSignal, type Component, type JSX } from 'solid-js';
import { createSignal, createMemo, type Component, type JSX } from 'solid-js';
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
type NotePostFormProps = {
@@ -8,11 +8,18 @@ type NotePostFormProps = {
const NotePostForm: Component<NotePostFormProps> = (props) => {
const [text, setText] = createSignal<string>('');
const handleChangeText: JSX.EventHandler<HTMLTextAreaElement, Event> = (ev) => {
setText(ev.currentTarget.value);
};
const handleSubmit: JSX.EventHandler<HTMLFormElement, Event> = (ev) => {
ev.preventDefault();
props.onPost({ content: text() });
// TODO 投稿完了したらなんかする
};
const submitDisabled = createMemo(() => text().trim().length === 0);
return (
<div class="p-1">
<form class="grid w-64 gap-1" onSubmit={handleSubmit}>
@@ -21,13 +28,16 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
class="rounded border-none"
rows={4}
placeholder="いまどうしてる?"
onChange={(ev) => {
setText(ev.target.value);
}}
onInput={handleChangeText}
value={text()}
/>
<div class="grid justify-end">
<button class="h-8 w-8 rounded bg-primary p-2 font-bold text-white" type="submit">
<button
class="h-8 w-8 rounded bg-primary p-2 font-bold text-white"
classList={{ 'bg-primary-disabled': submitDisabled(), 'bg-primary': !submitDisabled() }}
type="submit"
disabled={submitDisabled()}
>
<PaperAirplane />
</button>
</div>

View File

@@ -0,0 +1,28 @@
import { For, Switch, Match, type Component } from 'solid-js';
import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
import TextNote from '@/components/TextNote';
import Reaction from '@/components/notification/Reaction';
export type TimelineProps = {
events: NostrEvent[];
};
const Timeline: Component<TimelineProps> = (props) => {
return (
<For each={props.events}>
{(event) => (
<Switch fallback={<div>unknown event</div>}>
<Match when={event.kind === Kind.Text}>
<TextNote event={event} />
</Match>
<Match when={event.kind === Kind.Reaction}>
<Reaction event={event} />
</Match>
</Switch>
)}
</For>
);
};
export default Timeline;

View File

@@ -1,12 +1,23 @@
import { Show, For, createSignal, createMemo, onMount, onCleanup } from 'solid-js';
import type { Component } from 'solid-js';
import {
Show,
For,
createSignal,
createMemo,
onMount,
onCleanup,
type JSX,
Component,
} from 'solid-js';
import type { Event as NostrEvent } from 'nostr-tools/event';
import HeartOutlined from 'heroicons/24/outline/heart.svg';
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
import ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg';
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg';
import useProfile from '@/clients/useProfile';
import useConfig from '@/clients/useConfig';
import useCommands from '@/clients/useCommands';
import useDatePulser from '@/hooks/useDatePulser';
import { formatRelative } from '@/utils/formatDate';
import ColumnItem from '@/components/ColumnItem';
@@ -20,21 +31,37 @@ export type TextNoteProps = {
const TextNote: Component<TextNoteProps> = (props) => {
const currentDate = useDatePulser();
const [config] = useConfig();
const commands = useCommands();
const { profile: author } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.event.pubkey,
}));
const replyingToPubKeys = createMemo(() =>
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 handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
ev.preventDefault();
commands.publishDeprecatedRepost({});
};
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
ev.preventDefault();
commands.publishReaction({
relayUrls: config().relayUrls,
pubkey: pubkeyHex,
eventId: props.event.id,
});
};
return (
<div class="textnote">
<ColumnItem>
<div class="author-icon max-w-10 max-h-10 shrink-0">
<Show when={author()?.picture} fallback={<div class="h-10 w-10" />}>
<div class="author-icon h-10 w-10 shrink-0">
<Show when={author()?.picture}>
<img
src={author()?.picture}
alt="icon"
@@ -47,7 +74,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
<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}>
<Show when={author()?.display_name != null && author()?.display_name.length > 0}>
<div class="author-name pr-1 font-bold">{author()?.display_name}</div>
</Show>
<div class="author-username truncate text-zinc-600">
@@ -71,15 +98,21 @@ const TextNote: Component<TextNoteProps> = (props) => {
</div>
</Show>
<div class="content whitespace-pre-wrap break-all">
<TextNoteContentDisplay event={props.event} />
<TextNoteContentDisplay event={props.event} embedding={true} />
</div>
<div class="flex justify-evenly">
<div class="flex justify-end gap-16">
<button class="h-4 w-4 text-zinc-400">
<ChatBubbleLeft />
</button>
<button class="h-4 w-4 text-zinc-400" onClick={handleRepost}>
<ArrowPathRoundedSquare />
</button>
<button class="h-4 w-4 text-zinc-400">
<button class="h-4 w-4 text-zinc-400" onClick={handleReaction}>
<HeartOutlined />
</button>
<button class="h-4 w-4 text-zinc-400">
<EllipsisHorizontal />
</button>
</div>
</div>
</ColumnItem>

View File

@@ -1,5 +1,5 @@
import { For, Switch, Match, type Component } from 'solid-js';
import type { Event as NostrEvent } from 'nostr-tools/event';
import { Kind, type Event as NostrEvent } from 'nostr-tools/event';
import TextNote from '@/components/TextNote';
import DeprecatedRepost from '@/components/DeprecatedRepost';
@@ -8,15 +8,15 @@ export type TimelineProps = {
events: NostrEvent[];
};
export const Timeline: Component<TimelineProps> = (props) => {
const Timeline: Component<TimelineProps> = (props) => {
return (
<For each={props.events}>
{(event) => (
<Switch fallback={<div>unknown event</div>}>
<Match when={event.kind === 1}>
<Match when={event.kind === Kind.Text}>
<TextNote event={event} />
</Match>
<Match when={event.kind === 6}>
<Match when={(event.kind as number) === 6}>
<DeprecatedRepost event={event} />
</Match>
</Switch>

View File

@@ -0,0 +1,50 @@
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 useConfig from '@/clients/useConfig';
import useProfile from '@/clients/useProfile';
import useEvent from '@/clients/useEvent';
import TextNote from '../TextNote';
type ReactionProps = {
event: NostrEvent;
};
const Reaction: Component<ReactionProps> = (props) => {
const [config] = useConfig();
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.event.pubkey,
}));
const { event } = useEvent(() => ({ relayUrls: config().relayUrls, eventId: eventId() }));
return (
<div>
<div class="flex gap-1 text-sm">
<div>
<Switch fallback={props.event.content}>
<Match when={props.event.content === '+'}>
<span class="inline-block h-4 w-4 text-rose-400">
<HeartSolid />
</span>
</Match>
</Switch>
</div>
<div>
<span class="font-bold">{profile()?.display_name}</span>
{' reacted'}
</div>
</div>
<div>
<Show when={event() != null} fallback={'loading'}>
<TextNote event={event()} />
</Show>
</div>
</div>
);
};
export default Reaction;

View File

@@ -5,7 +5,7 @@ export type MentionedEventDisplayProps = {
};
const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
return <span class="text-blue-500 underline">@{props.mentionedEvent.eventId}</span>;
return <span class="text-blue-500 underline">#{props.mentionedEvent.eventId}</span>;
};
export default MentionedEventDisplay;

View File

@@ -7,6 +7,7 @@ import MentionedEventDisplay from '@/components/textNote/MentionedEventDisplay';
export type TextNoteContentDisplayProps = {
event: NostrEvent;
embedding: boolean;
};
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
@@ -19,7 +20,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
if (item.type === 'MentionedUser') {
return <MentionedUserDisplay mentionedUser={item} />;
}
if (item.type === 'MentionedEvent') {
if (item.type === 'MentionedEvent' && props.embedding) {
return <MentionedEventDisplay mentionedEvent={item} />;
}
if (item.type === 'HashTag') {