mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
update
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
50
src/components/TimelineContentDisplay.tsx
Normal file
50
src/components/TimelineContentDisplay.tsx
Normal 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;
|
||||
32
src/components/TimelineContext.tsx
Normal file
32
src/components/TimelineContext.tsx
Normal 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),
|
||||
};
|
||||
};
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user