This commit is contained in:
Shusui MOYATANI
2023-04-03 10:47:53 +09:00
parent 3e52a24f87
commit ed03386e50
21 changed files with 483 additions and 348 deletions

View File

@@ -2,8 +2,8 @@ import { Show, type JSX, type Component } from 'solid-js';
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
import { useHandleCommand } from '@/hooks/useCommandBus';
import { ColumnContext, useColumnState } from '@/components/ColumnContext';
import ColumnContentDisplay from '@/components/ColumnContentDisplay';
import { TimelineContext, useTimelineState } from '@/components/TimelineContext';
import TimelineContentDisplay from '@/components/TimelineContentDisplay';
export type ColumnProps = {
name: string;
@@ -16,7 +16,7 @@ export type ColumnProps = {
const Column: Component<ColumnProps> = (props) => {
let columnDivRef: HTMLDivElement | undefined;
const columnState = useColumnState();
const timelineState = useTimelineState();
const width = () => props.width ?? 'medium';
@@ -39,7 +39,7 @@ const Column: Component<ColumnProps> = (props) => {
}));
return (
<ColumnContext.Provider value={columnState}>
<TimelineContext.Provider value={timelineState}>
<div
ref={columnDivRef}
class="relative flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
@@ -54,14 +54,14 @@ const Column: Component<ColumnProps> = (props) => {
{/* <span class="column-icon">🏠</span> */}
<span class="column-name">{props.name}</span>
</div>
<ul class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
<Show when={columnState.columnState.content} keyed>
{(columnContent) => (
<div class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</div>
<Show when={timelineState.timelineState.content} keyed>
{(timeline) => (
<div class="absolute h-full w-full bg-white">
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
<button
class="flex w-full items-center gap-1"
onClick={() => columnState?.clearColumnContext()}
onClick={() => timelineState?.clearTimeline()}
>
<div class="inline-block h-4 w-4">
<ArrowLeft />
@@ -70,13 +70,13 @@ const Column: Component<ColumnProps> = (props) => {
</button>
</div>
<ul class="flex h-full flex-col overflow-y-scroll scroll-smooth">
<ColumnContentDisplay columnContent={columnContent} />
<TimelineContentDisplay timelineContent={timeline} />
</ul>
</div>
)}
</Show>
</div>
</ColumnContext.Provider>
</TimelineContext.Provider>
);
};

View File

@@ -1,33 +0,0 @@
import { Switch, Match, type Component } from 'solid-js';
import useConfig from '@/nostr/useConfig';
import { type ColumnContent } from '@/components/ColumnContext';
import Timeline from '@/components/Timeline';
import useSubscription from '@/nostr/useSubscription';
const RepliesDisplay: Component<{ eventId: string }> = (props) => {
const { config } = useConfig();
const { events } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{ kinds: [1], ids: [props.eventId], limit: 25 },
{ kinds: [1], '#e': [props.eventId], limit: 25 },
],
}));
return <Timeline events={[...events()].reverse()} embedding={false} />;
};
const ColumnContentDisplay: Component<{ columnContent: ColumnContent }> = (props) => {
return (
<Switch>
<Match when={props.columnContent.type === 'Replies' && props.columnContent} keyed>
{(replies) => <RepliesDisplay eventId={replies.eventId} />}
</Match>
</Switch>
);
};
export default ColumnContentDisplay;

View File

@@ -1,31 +0,0 @@
import { createContext, useContext, type JSX } from 'solid-js';
import { createStore } from 'solid-js/store';
export type ColumnContent = {
type: 'Replies';
eventId: string;
};
export type ColumnState = {
content?: ColumnContent;
};
export type UseColumnState = {
columnState: ColumnState;
setColumnContent: (content: ColumnContent) => void;
clearColumnContext: () => void;
};
export const ColumnContext = createContext<UseColumnState>();
export const useColumnContext = () => useContext(ColumnContext);
export const useColumnState = (): UseColumnState => {
const [columnState, setColumnState] = createStore<ColumnState>({});
return {
columnState,
setColumnContent: (content: ColumnContent) => setColumnState('content', content),
clearColumnContext: () => setColumnState('content', undefined),
};
};

View File

@@ -183,6 +183,23 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
uploadFilesMutation.mutate(files);
};
const handlePaste: JSX.EventHandler<HTMLTextAreaElement, ClipboardEvent> = (ev) => {
if (uploadFilesMutation.isLoading) return;
const items = [...(ev?.clipboardData?.items ?? [])];
const files: File[] = [];
items.forEach((item) => {
if (item.kind === 'file') {
ev.preventDefault();
const file = item.getAsFile();
if (file == null) return;
files.push(file);
}
});
if (files.length === 0) return;
uploadFilesMutation.mutate(files);
};
const handleDragOver: JSX.EventHandler<HTMLTextAreaElement, DragEvent> = (ev) => {
ev.preventDefault();
};
@@ -239,6 +256,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
onKeyDown={handleKeyDown}
onDragOver={handleDragOver}
onDrop={handleDrop}
onPaste={handlePaste}
value={text()}
/>
<div class="flex items-end justify-end gap-1">

View File

@@ -6,16 +6,15 @@ import DeprecatedRepost from '@/components/DeprecatedRepost';
export type TimelineProps = {
events: NostrEvent[];
embedding?: boolean;
};
const Timeline: Component<TimelineProps> = (props) => {
return (
<For each={props.events}>
{(event) => (
<Switch fallback={<div>unknown event</div>}>
<Switch fallback={<div>{event.kind}</div>}>
<Match when={event.kind === Kind.Text}>
<TextNote event={event} embedding={props.embedding ?? true} />
<TextNote event={event} />
</Match>
<Match when={(event.kind as number) === 6}>
<DeprecatedRepost event={event} />

View File

@@ -0,0 +1,50 @@
import { Switch, Match, type Component } from 'solid-js';
import { Filter, Event as NostrEvent } from 'nostr-tools';
import uniq from 'lodash/uniq';
import useConfig from '@/nostr/useConfig';
import { type TimelineContent } from '@/components/TimelineContext';
import Timeline from '@/components/Timeline';
import useSubscription from '@/nostr/useSubscription';
import eventWrapper from '@/core/event';
const relatedEvents = (rawEvent: NostrEvent) => {
const event = () => eventWrapper(rawEvent);
const ids = [rawEvent.id];
const rootId = event().rootEvent()?.id;
if (rootId != null) ids.push(rootId);
const replyId = event().replyingToEvent()?.id;
if (replyId != null) ids.push(replyId);
return uniq(ids);
};
const RepliesDisplay: Component<{ event: NostrEvent }> = (props) => {
const { config } = useConfig();
const { events } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{ kinds: [1], ids: relatedEvents(props.event), limit: 25 },
{ kinds: [1], '#e': [props.event.id], limit: 25 } as Filter,
],
limit: 200,
}));
return <Timeline events={[...events()].reverse()} />;
};
const TimelineContentDisplay: Component<{ timelineContent: TimelineContent }> = (props) => {
return (
<Switch>
<Match when={props.timelineContent.type === 'Replies' && props.timelineContent} keyed>
{(replies) => <RepliesDisplay event={replies.event} />}
</Match>
</Switch>
);
};
export default TimelineContentDisplay;

View File

@@ -0,0 +1,32 @@
import { createContext, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';
import { Event as NostrEvent } from 'nostr-tools';
export type TimelineContent = {
type: 'Replies';
event: NostrEvent;
};
export type TimelineState = {
content?: TimelineContent;
};
export type UseTimelineState = {
timelineState: TimelineState;
setTimeline: (content: TimelineContent) => void;
clearTimeline: () => void;
};
export const TimelineContext = createContext<UseTimelineState>();
export const useTimelineContext = () => useContext(TimelineContext);
export const useTimelineState = (): UseTimelineState => {
const [timelineState, setTimelineState] = createStore<TimelineState>({});
return {
timelineState,
setTimeline: (content: TimelineContent) => setTimelineState('content', content),
clearTimeline: () => setTimelineState('content', undefined),
};
};

View File

@@ -42,7 +42,7 @@ const Reaction: Component<ReactionProps> = (props) => {
</Match>
</Switch>
</div>
<div class="notification-user flex gap-1">
<div class="notification-user flex gap-1 overflow-hidden">
<div class="author-icon h-5 w-5 shrink-0 overflow-hidden object-cover">
<Show when={profile()?.picture != null}>
<img
@@ -53,9 +53,9 @@ const Reaction: Component<ReactionProps> = (props) => {
/>
</Show>
</div>
<div>
<div class="flex-1 overflow-hidden">
<button
class="truncate whitespace-pre-wrap break-all font-bold hover:text-blue-500 hover:underline"
class="truncate font-bold hover:text-blue-500 hover:underline"
onClick={() => showProfile(props.event.pubkey)}
>
<UserDisplayName pubkey={props.event.pubkey} />

View File

@@ -1,4 +1,4 @@
import { Component, createEffect, createSignal, Show } from 'solid-js';
import { Component, createEffect, createSignal, onMount, Show, JSX } from 'solid-js';
import { fixUrl } from '@/utils/imageUrl';
import SafeLink from '../utils/SafeLink';
@@ -8,7 +8,37 @@ type ImageDisplayProps = {
};
const ImageDisplay: Component<ImageDisplayProps> = (props) => {
let imageRef: HTMLImageElement | undefined;
let canvasRef: HTMLCanvasElement | undefined;
const [hidden, setHidden] = createSignal(props.initialHidden);
const [playing, setPlaying] = createSignal(true);
const isGIF = () => props.url.match(/\.gif/i);
const play = () => {
setPlaying(true);
};
const stop = () => {
if (canvasRef == null || imageRef == null) return;
canvasRef.width = imageRef.width;
canvasRef.height = imageRef.height;
canvasRef
.getContext('2d')
?.drawImage(
imageRef,
0,
0,
imageRef.naturalWidth,
imageRef.naturalHeight,
0,
0,
imageRef.width,
imageRef.height,
);
setPlaying(false);
};
return (
<Show
@@ -24,11 +54,45 @@ const ImageDisplay: Component<ImageDisplayProps> = (props) => {
>
<SafeLink class="my-2 block" href={props.url}>
<img
class="inline-block max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
src={fixUrl(props.url)}
ref={imageRef}
class="max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
classList={{
'inline-block': playing(),
hidden: !playing(),
}}
src={playing() ? fixUrl(props.url) : undefined}
alt={props.url}
/>
<canvas
ref={canvasRef}
class="inline-block max-h-64 max-w-full rounded object-contain shadow hover:shadow-md"
classList={{
'w-0': playing(),
'h-0': playing(),
'w-auto': !playing(),
'h-auto': !playing(),
}}
onClick={(ev) => {
ev.preventDefault();
play();
}}
/>
</SafeLink>
{/*
<Show when={isGIF()}>
<button
class=""
onClick={() => {
if (playing()) stop();
else play();
}}
>
<Show when={!playing()} fallback="⏸">
</Show>
</button>
</Show>
*/}
</Show>
);
};

View File

@@ -43,10 +43,10 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
</div>
);
}
if (item.data.type === 'npub' && props.embedding) {
if (item.data.type === 'npub') {
return <MentionedUserDisplay pubkey={item.data.data} />;
}
if (item.data.type === 'nprofile' && props.embedding) {
if (item.data.type === 'nprofile') {
return <MentionedUserDisplay pubkey={item.data.data.pubkey} />;
}
return <span class="text-blue-500 underline">{item.content}</span>;

View File

@@ -22,7 +22,7 @@ import useModalState from '@/hooks/useModalState';
import UserNameDisplay from '@/components/UserDisplayName';
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
import { useColumnContext } from '@/components/ColumnContext';
import { useTimelineContext } from '@/components/TimelineContext';
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay';
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
@@ -45,7 +45,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const formatDate = useFormatDate();
const pubkey = usePubkey();
const { showProfile } = useModalState();
const columnContext = useColumnContext();
const timelineContext = useTimelineContext();
const [showReplyForm, setShowReplyForm] = createSignal(false);
const closeReplyForm = () => setShowReplyForm(false);
@@ -209,9 +209,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
type="button"
class="hover:underline"
onClick={() => {
columnContext?.setColumnContent({
timelineContext?.setTimeline({
type: 'Replies',
eventId: event().rootEvent()?.id ?? props.event.id,
event: props.event,
});
}}
>