mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 14:34:25 +01:00
feat: column customization
This commit is contained in:
@@ -5,13 +5,12 @@ import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-squa
|
||||
import { Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import UserDisplayName from '@/components/UserDisplayName';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
|
||||
import TextNoteDisplayById from './textNote/TextNoteDisplayById';
|
||||
|
||||
export type DeprecatedRepostProps = {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const Modal: Component<ModalProps> = (props) => {
|
||||
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute top-0 left-0 z-10 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/30"
|
||||
class="absolute left-0 top-0 z-10 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/30"
|
||||
onClick={handleClickContainer}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSignal, Show, type JSX, Component } from 'solid-js';
|
||||
|
||||
import Cog6Tooth from 'heroicons/24/outline/cog-6-tooth.svg';
|
||||
import Plus from 'heroicons/24/outline/plus.svg';
|
||||
import MagnifyingGlass from 'heroicons/24/solid/magnifying-glass.svg';
|
||||
import PencilSquare from 'heroicons/24/solid/pencil-square.svg';
|
||||
|
||||
@@ -8,10 +9,13 @@ import Config from '@/components/modal/Config';
|
||||
import NotePostForm from '@/components/NotePostForm';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import resolveAsset from '@/utils/resolveAsset';
|
||||
|
||||
const SideBar: Component = () => {
|
||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||
|
||||
const { showAddColumn, showAbout } = useModalState();
|
||||
const { config } = useConfig();
|
||||
const [formOpened, setFormOpened] = createSignal(false);
|
||||
const [configOpened, setConfigOpened] = createSignal(false);
|
||||
@@ -39,7 +43,7 @@ const SideBar: Component = () => {
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r border-rose-200 pt-5">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<button
|
||||
class="h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl font-bold text-white"
|
||||
class="h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl text-white"
|
||||
onClick={() => toggleForm()}
|
||||
>
|
||||
<PencilSquare />
|
||||
@@ -49,17 +53,28 @@ const SideBar: Component = () => {
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
*/}
|
||||
{/* <div>column 1</div> */}
|
||||
{/* <div>column 2</div> */}
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<div class="flex flex-col items-center pb-2">
|
||||
<button
|
||||
class="h-12 w-12 p-3 text-primary"
|
||||
class="h-10 w-12 rounded-full p-3 text-2xl text-primary"
|
||||
onClick={() => showAddColumn()}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
<button
|
||||
class="h-10 w-12 p-3 text-primary"
|
||||
onClick={() => setConfigOpened((current) => !current)}
|
||||
>
|
||||
<Cog6Tooth />
|
||||
</button>
|
||||
<button class="pt-2" onClick={() => showAbout()}>
|
||||
<img
|
||||
class="h-8 w-8"
|
||||
src={resolveAsset('/images/rabbit_app_256.png')}
|
||||
alt="About rabbit"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Show, type Component } from 'solid-js';
|
||||
|
||||
import ColumnItem from '@/components/ColumnItem';
|
||||
import TextNoteDisplay, { TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay';
|
||||
import useConfig from '@/core/useConfig';
|
||||
|
||||
import TextNoteDisplay, { TextNoteDisplayProps } from './textNote/TextNoteDisplay';
|
||||
|
||||
export type TextNoteProps = TextNoteDisplayProps;
|
||||
|
||||
const TextNote: Component<TextNoteProps> = (props) => {
|
||||
|
||||
35
src/components/column/BasicColumnHeader.tsx
Normal file
35
src/components/column/BasicColumnHeader.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component, JSX, Show, createSignal } from 'solid-js';
|
||||
|
||||
import EllipsisVertical from 'heroicons/24/outline/ellipsis-vertical.svg';
|
||||
|
||||
export type BasicColumnHeaderProps = {
|
||||
name: string;
|
||||
icon?: JSX.Element;
|
||||
settings: () => JSX.Element;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const BasicColumnHeader: Component<BasicColumnHeaderProps> = (props) => {
|
||||
const [isSettingsOpened, setIsSettingOpened] = createSignal(false);
|
||||
|
||||
const toggleSettingsOpened = () => setIsSettingOpened((current) => !current);
|
||||
|
||||
return (
|
||||
<div class="flex flex-col">
|
||||
<div class="flex h-8 items-center gap-1 px-2">
|
||||
<h2 class="flex flex-1 items-center gap-1">
|
||||
<Show when={props.icon} keyed>
|
||||
{(icon) => <span class="inline-block h-4 w-4 text-gray-700">{icon}</span>}
|
||||
</Show>
|
||||
<span class="column-name">{props.name}</span>
|
||||
</h2>
|
||||
<button class="h-4 w-4" onClick={() => toggleSettingsOpened()}>
|
||||
<EllipsisVertical />
|
||||
</button>
|
||||
</div>
|
||||
<Show when={isSettingsOpened()}>{props.settings()}</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicColumnHeader;
|
||||
@@ -2,15 +2,15 @@ import { Show, type JSX, type Component } from 'solid-js';
|
||||
|
||||
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
||||
|
||||
import TimelineContentDisplay from '@/components/TimelineContentDisplay';
|
||||
import { TimelineContext, useTimelineState } from '@/components/TimelineContext';
|
||||
import TimelineContentDisplay from '@/components/timeline/TimelineContentDisplay';
|
||||
import { TimelineContext, useTimelineState } from '@/components/timeline/TimelineContext';
|
||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||
|
||||
export type ColumnProps = {
|
||||
name: string;
|
||||
columnIndex: number;
|
||||
lastColumn?: true;
|
||||
lastColumn: boolean;
|
||||
width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
|
||||
header: JSX.Element;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
@@ -46,9 +46,9 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
||||
classList={{
|
||||
'sm:w-[500px]': width() === 'widest',
|
||||
'sm:w-[350px]': width() === 'wide',
|
||||
'sm:w-[310px]': width() === 'medium',
|
||||
'sm:w-[270px]': width() === 'narrow',
|
||||
'sm:w-[360px]': width() === 'wide',
|
||||
'sm:w-[320px]': width() === 'medium',
|
||||
'sm:w-[280px]': width() === 'narrow',
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
@@ -56,17 +56,14 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
keyed
|
||||
fallback={
|
||||
<>
|
||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||
{/* <span class="column-icon">🏠</span> */}
|
||||
<span class="column-name">{props.name}</span>
|
||||
</div>
|
||||
<div class="shrink-0 border-b">{props.header}</div>
|
||||
<div class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{(timeline) => (
|
||||
<div class="h-full w-full bg-white">
|
||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||
<div class="flex shrink-0 items-center border-b bg-white px-2">
|
||||
<button
|
||||
class="flex w-full items-center gap-1"
|
||||
onClick={() => timelineState?.clearTimeline()}
|
||||
101
src/components/column/ColumnSettings.tsx
Normal file
101
src/components/column/ColumnSettings.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Component, JSX } from 'solid-js';
|
||||
|
||||
import ChevronLeft from 'heroicons/24/outline/chevron-left.svg';
|
||||
import ChevronRight from 'heroicons/24/outline/chevron-right.svg';
|
||||
import Trash from 'heroicons/24/outline/trash.svg';
|
||||
|
||||
import { ColumnType } from '@/core/column';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useRequestCommand } from '@/hooks/useCommandBus';
|
||||
|
||||
type ColumnSettingsProps = {
|
||||
column: ColumnType;
|
||||
columnIndex: number;
|
||||
};
|
||||
|
||||
type ColumnSettingsSectionProps = {
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const ColumnSettingsSection: Component<ColumnSettingsSectionProps> = (props) => {
|
||||
return (
|
||||
<div class="flex flex-col gap-2 border-b p-2">
|
||||
<div>{props.title}</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
||||
const { saveColumn, removeColumn, moveColumn } = useConfig();
|
||||
const requestCommand = useRequestCommand();
|
||||
|
||||
const setColumnWidth = (width: ColumnType['width']) => {
|
||||
saveColumn({ ...props.column, width });
|
||||
};
|
||||
|
||||
const move = (index: number) => {
|
||||
moveColumn(props.column.id, index);
|
||||
requestCommand({ command: 'moveToColumn', columnIndex: index }).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col border-t">
|
||||
<ColumnSettingsSection title="カラム幅">
|
||||
<div class="flex h-9 gap-2">
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
onClick={() => setColumnWidth('widest')}
|
||||
>
|
||||
特大
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
onClick={() => setColumnWidth('wide')}
|
||||
>
|
||||
大
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
onClick={() => setColumnWidth('medium')}
|
||||
>
|
||||
中
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
onClick={() => setColumnWidth('narrow')}
|
||||
>
|
||||
小
|
||||
</button>
|
||||
</div>
|
||||
</ColumnSettingsSection>
|
||||
<div class="flex h-10 items-center gap-2">
|
||||
<button class="py-4 pl-2" title="左に移動" onClick={() => move(props.columnIndex - 1)}>
|
||||
<span class="inline-block h-4 w-4">
|
||||
<ChevronLeft />
|
||||
</span>
|
||||
</button>
|
||||
<button class="py-4 pr-2" title="右に移動" onClick={() => move(props.columnIndex + 1)}>
|
||||
<span class="inline-block h-4 w-4">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
class="px-2 py-4 text-rose-500 hover:text-rose-600"
|
||||
title="削除"
|
||||
onClick={() => removeColumn(props.column.id)}
|
||||
>
|
||||
<span class="inline-block h-4 w-4" aria-label="削除">
|
||||
<Trash />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnSettings;
|
||||
74
src/components/column/Columns.tsx
Normal file
74
src/components/column/Columns.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { For, Switch, Match } from 'solid-js';
|
||||
|
||||
import FollowingColumn from '@/components/column/FollwingColumn';
|
||||
import NotificationColumn from '@/components/column/NotificationColumn';
|
||||
import PostsColumn from '@/components/column/PostsColumn';
|
||||
import ReactionsColumn from '@/components/column/ReactionsColumn';
|
||||
import RelaysColumn from '@/components/column/RelaysColumn';
|
||||
import useConfig from '@/core/useConfig';
|
||||
|
||||
const Columns = () => {
|
||||
const { config } = useConfig();
|
||||
|
||||
return (
|
||||
<div class="flex h-full snap-x snap-mandatory flex-row overflow-y-hidden overflow-x-scroll">
|
||||
<For each={config().columns}>
|
||||
{(column, index) => {
|
||||
const columnIndex = () => index() + 1;
|
||||
const lastColumn = () => columnIndex() === config().columns.length;
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={column.columnType === 'Following' && column} keyed>
|
||||
{(followingColumn) => (
|
||||
<FollowingColumn
|
||||
column={followingColumn}
|
||||
columnIndex={columnIndex()}
|
||||
lastColumn={lastColumn()}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={column.columnType === 'Notification' && column} keyed>
|
||||
{(notificationColumn) => (
|
||||
<NotificationColumn
|
||||
column={notificationColumn}
|
||||
columnIndex={columnIndex()}
|
||||
lastColumn={lastColumn()}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={column.columnType === 'Posts' && column} keyed>
|
||||
{(postsColumn) => (
|
||||
<PostsColumn
|
||||
column={postsColumn}
|
||||
columnIndex={columnIndex()}
|
||||
lastColumn={lastColumn()}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={column.columnType === 'Reactions' && column} keyed>
|
||||
{(reactionsColumn) => (
|
||||
<ReactionsColumn
|
||||
column={reactionsColumn}
|
||||
columnIndex={columnIndex()}
|
||||
lastColumn={lastColumn()}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={column.columnType === 'Relays' && column} keyed>
|
||||
{(reactionsColumn) => (
|
||||
<RelaysColumn
|
||||
column={reactionsColumn}
|
||||
columnIndex={columnIndex()}
|
||||
lastColumn={lastColumn()}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Columns;
|
||||
67
src/components/column/FollwingColumn.tsx
Normal file
67
src/components/column/FollwingColumn.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import Home from 'heroicons/24/outline/home.svg';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import { FollowingColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useFollowings } from '@/nostr/useBatchedEvents';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import epoch from '@/utils/epoch';
|
||||
|
||||
type FollowingColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: FollowingColumnType;
|
||||
};
|
||||
|
||||
const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
||||
const { config, removeColumn } = useConfig();
|
||||
|
||||
const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));
|
||||
|
||||
const { events: followingsPosts } = useSubscription(() => {
|
||||
const authors = uniq([...followingPubkeys()]);
|
||||
if (authors.length === 0) return null;
|
||||
return {
|
||||
relayUrls: config().relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6],
|
||||
authors,
|
||||
limit: 10,
|
||||
since: epoch() - 4 * 60 * 60,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? 'ホーム'}
|
||||
icon={<Home />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Timeline events={followingsPosts()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default FollowingColumn;
|
||||
57
src/components/column/NotificationColumn.tsx
Normal file
57
src/components/column/NotificationColumn.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import Bell from 'heroicons/24/outline/bell.svg';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Notification from '@/components/timeline/Notification';
|
||||
import { NotificationColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
type NotificationColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: NotificationColumnType;
|
||||
};
|
||||
|
||||
const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) => {
|
||||
const { config, removeColumn } = useConfig();
|
||||
|
||||
const { events: notifications } = useSubscription(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6, 7],
|
||||
'#p': [props.column.pubkey],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? '通知'}
|
||||
icon={<Bell />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Notification events={notifications()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationColumn;
|
||||
57
src/components/column/PostsColumn.tsx
Normal file
57
src/components/column/PostsColumn.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import User from 'heroicons/24/outline/user.svg';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import { PostsColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
type PostsColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: PostsColumnType;
|
||||
};
|
||||
|
||||
const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
||||
const { config, removeColumn } = useConfig();
|
||||
|
||||
const { events } = useSubscription(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6],
|
||||
authors: [props.column.pubkey],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? '投稿'}
|
||||
icon={<User />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Timeline events={events()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostsColumn;
|
||||
57
src/components/column/ReactionsColumn.tsx
Normal file
57
src/components/column/ReactionsColumn.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import Heart from 'heroicons/24/outline/heart.svg';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Notification from '@/components/timeline/Notification';
|
||||
import { ReactionsColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
type ReactionsColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: ReactionsColumnType;
|
||||
};
|
||||
|
||||
const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
||||
const { config, removeColumn } = useConfig();
|
||||
|
||||
const { events: reactions } = useSubscription(() => ({
|
||||
relayUrls: config().relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [7],
|
||||
authors: [props.column.pubkey],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? 'リアクション'}
|
||||
icon={<Heart />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Notification events={reactions()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactionsColumn;
|
||||
58
src/components/column/RelaysColumn.tsx
Normal file
58
src/components/column/RelaysColumn.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import { RelaysColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import epoch from '@/utils/epoch';
|
||||
|
||||
type RelaysColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: RelaysColumnType;
|
||||
};
|
||||
|
||||
const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
||||
const { removeColumn } = useConfig();
|
||||
|
||||
const { events } = useSubscription(() => ({
|
||||
relayUrls: props.column.relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6],
|
||||
limit: 25,
|
||||
since: epoch() - 4 * 60 * 60,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? 'リレー'}
|
||||
icon={<GlobeAlt />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Timeline events={events()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelaysColumn;
|
||||
58
src/components/column/SearchColumn.tsx
Normal file
58
src/components/column/SearchColumn.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
||||
|
||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||
import Column from '@/components/column/Column';
|
||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import { SearchColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import { relaysForSearching } from '@/core/relayUrls';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
|
||||
type SearchColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
lastColumn: boolean;
|
||||
column: SearchColumnType;
|
||||
};
|
||||
|
||||
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||
const { removeColumn } = useConfig();
|
||||
|
||||
const { events } = useSubscription(() => ({
|
||||
relayUrls: relaysForSearching,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6],
|
||||
search: props.column.query,
|
||||
limit: 25,
|
||||
},
|
||||
],
|
||||
clientEventFilter: (event) => {
|
||||
if (props.column.contentFilter == null) return true;
|
||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
<BasicColumnHeader
|
||||
name={props.column.name ?? '検索'}
|
||||
icon={<MagnifyingGlass />}
|
||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||
onClose={() => removeColumn(props.column.id)}
|
||||
/>
|
||||
}
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
>
|
||||
<Timeline events={events()} />
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchColumn;
|
||||
104
src/components/modal/About.tsx
Normal file
104
src/components/modal/About.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Component, createResource, For, Show } from 'solid-js';
|
||||
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import resolveAsset from '@/utils/resolveAsset';
|
||||
|
||||
type AboutProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type PackageInfo = {
|
||||
self: {
|
||||
name: string;
|
||||
author: string;
|
||||
version: string;
|
||||
homepage: string;
|
||||
licenseSpdx: string;
|
||||
licenseText: string;
|
||||
};
|
||||
packages: {
|
||||
name: string;
|
||||
version: string;
|
||||
licenseSpdx: string;
|
||||
licenseText: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const fetchPackageInfo = async (): Promise<PackageInfo> => {
|
||||
const res = await fetch(resolveAsset('packageInfo.json'));
|
||||
const body = await res.text();
|
||||
return JSON.parse(body) as PackageInfo;
|
||||
};
|
||||
|
||||
const commit = import.meta.env.COMMIT as string | null;
|
||||
|
||||
const About: Component<AboutProps> = (props) => {
|
||||
const [packageInfo] = createResource(fetchPackageInfo);
|
||||
|
||||
return (
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="p-8">
|
||||
<div class="flex flex-col items-center pt-8">
|
||||
<img src={resolveAsset('/images/rabbit_app_256.png')} alt="Logo" width="64" height="64" />
|
||||
|
||||
<h1 class="my-4">
|
||||
Rabbit <span id="app-version">v{packageInfo()?.self?.version}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<h2 class="my-4 text-xl font-bold">利用規約</h2>
|
||||
|
||||
<p class="my-4">Copyright (C) 2023 Shusui Moyatani</p>
|
||||
|
||||
<p class="my-4">
|
||||
このプログラムは自由ソフトウェアです。フリーソフトウェア財団から発行された
|
||||
GNUアフェロー一般公衆ライセンス(バージョン3か、(任意で)より新しいバージョンのいずれか)の条件の下で
|
||||
再頒布や改変、あるいはその両方を行うことができます。
|
||||
</p>
|
||||
|
||||
<p class="my-4">
|
||||
このプログラムは役立つことを願って頒布されていますが、
|
||||
<strong class="font-bold">いかなる保証もありません</strong>。<em>商品性</em>や
|
||||
<em>特定目的適合性</em> に対する保証は暗示されたものも含めて存在しません。
|
||||
詳しくはGNUアフェロー一般公衆ライセンスをご覧ください。
|
||||
</p>
|
||||
|
||||
<p class="my-4">
|
||||
あなたは、このプログラムに付随してGNUアフェロー一般公衆ライセンスのコピーを受け取っていることでしょう。
|
||||
そうでなければ、
|
||||
<a class="link" href="https://www.gnu.org/licenses/">
|
||||
https://www.gnu.org/licenses/
|
||||
</a>
|
||||
をご参照ください。
|
||||
</p>
|
||||
|
||||
<a class="text-blue-500 underline" href="https://gpl.mhatta.org/agpl.ja.html">
|
||||
参考訳
|
||||
</a>
|
||||
|
||||
<pre class="max-h-96 overflow-scroll rounded bg-zinc-100 p-4 text-xs">
|
||||
{packageInfo()?.self.licenseText}
|
||||
</pre>
|
||||
|
||||
<h2 class="my-4 text-xl font-bold">使用ライブラリ</h2>
|
||||
|
||||
<For each={packageInfo()?.packages ?? []} fallback="取得中">
|
||||
{(p) => {
|
||||
return (
|
||||
<>
|
||||
<h3 class="mb-2 mt-4 font-mono">
|
||||
{p.name}@{p.version} ({p.licenseSpdx})
|
||||
</h3>
|
||||
<pre class="max-h-96 overflow-scroll rounded bg-zinc-100 p-4 text-xs">
|
||||
{p.licenseText}
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</BasicModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,20 +1,113 @@
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
const AddColumn = () => {
|
||||
const { addColumn } = useConfig();
|
||||
import Bell from 'heroicons/24/outline/bell.svg';
|
||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||
import Heart from 'heroicons/24/outline/heart.svg';
|
||||
import Home from 'heroicons/24/outline/home.svg';
|
||||
import User from 'heroicons/24/outline/user.svg';
|
||||
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import {
|
||||
createFollowingColumn,
|
||||
createJapanRelaysColumn,
|
||||
createNotificationColumn,
|
||||
createPostsColumn,
|
||||
createReactionsColumn,
|
||||
} from '@/core/column';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
|
||||
type AddColumnProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
const pubkey = usePubkey();
|
||||
const { saveColumn } = useConfig();
|
||||
|
||||
const addFollowingColumn = () => {
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||
saveColumn(createFollowingColumn({ pubkey: pubkeyNonNull }));
|
||||
});
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const addNotificationColumn = () => {
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||
saveColumn(createNotificationColumn({ pubkey: pubkeyNonNull }));
|
||||
});
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const addJapanRelaysColumn = () => {
|
||||
saveColumn(createJapanRelaysColumn());
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const addMyPostsColumn = () => {
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||
saveColumn(createPostsColumn({ pubkey: pubkeyNonNull }));
|
||||
});
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const addMyReactionsColumn = () => {
|
||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||
saveColumn(createReactionsColumn({ pubkey: pubkeyNonNull }));
|
||||
});
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicModal onClose={() => console.log('closed')}>
|
||||
<ul>
|
||||
<li>ホーム</li>
|
||||
<li>通知</li>
|
||||
<li>検索</li>
|
||||
<li>リレー</li>
|
||||
<li>ユーザー</li>
|
||||
<li>いいね</li>
|
||||
<li>ダイレクトメッセージ</li>
|
||||
</ul>
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="flex flex-wrap p-4">
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
onClick={() => addFollowingColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
<Home />
|
||||
</span>
|
||||
ホーム
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
onClick={() => addNotificationColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
<Bell />
|
||||
</span>
|
||||
通知
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
onClick={() => addJapanRelaysColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
<GlobeAlt />
|
||||
</span>
|
||||
日本リレー
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
onClick={() => addMyPostsColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
<User />
|
||||
</span>
|
||||
自分の投稿
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
onClick={() => addMyReactionsColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
<Heart />
|
||||
</span>
|
||||
自分のリアクション
|
||||
</button>
|
||||
</div>
|
||||
</BasicModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export type BasicModalProps = {
|
||||
const BasicModal: Component<BasicModalProps> = (props) => {
|
||||
return (
|
||||
<Modal onClose={() => props.onClose?.()}>
|
||||
<div class="h-screen w-[640px] max-w-full">
|
||||
<div class="h-full w-[640px] max-w-full">
|
||||
<button
|
||||
class="w-full pt-1 text-start text-stone-800"
|
||||
aria-label="Close"
|
||||
|
||||
45
src/components/modal/GlobalModal.tsx
Normal file
45
src/components/modal/GlobalModal.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Show, Switch, Match } from 'solid-js';
|
||||
|
||||
import About from '@/components/modal/About';
|
||||
import AddColumn from '@/components/modal/AddColumn';
|
||||
import ProfileDisplay from '@/components/modal/ProfileDisplay';
|
||||
import ProfileEdit from '@/components/modal/ProfileEdit';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
|
||||
const GlobalModal = () => {
|
||||
const pubkey = usePubkey();
|
||||
const { modalState, showProfile, closeModal } = useModalState();
|
||||
|
||||
return (
|
||||
<Show when={modalState()} keyed>
|
||||
{(state) => (
|
||||
<Switch>
|
||||
<Match when={state.type === 'Profile' && state.pubkey} keyed>
|
||||
{(pubkeyNonNull: string) => (
|
||||
<ProfileDisplay pubkey={pubkeyNonNull} onClose={closeModal} />
|
||||
)}
|
||||
</Match>
|
||||
<Match when={state.type === 'ProfileEdit'} keyed>
|
||||
<ProfileEdit
|
||||
onClose={() =>
|
||||
ensureNonNull([pubkey()])(([pubkeyNonNull]) => {
|
||||
showProfile(pubkeyNonNull);
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={state.type === 'AddColumn'}>
|
||||
<AddColumn onClose={closeModal} />
|
||||
</Match>
|
||||
<Match when={state.type === 'About'}>
|
||||
<About onClose={closeModal} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalModal;
|
||||
@@ -10,7 +10,7 @@ import uniq from 'lodash/uniq';
|
||||
|
||||
import ContextMenu, { MenuItem } from '@/components/ContextMenu';
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import Timeline from '@/components/Timeline';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
@@ -236,7 +236,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
<Match when={following()}>
|
||||
<button
|
||||
class="rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500 sm:w-32"
|
||||
text-center font-bold text-white hover:bg-rose-500 sm:w-36"
|
||||
onMouseEnter={() => setHoverFollowButton(true)}
|
||||
onMouseLeave={() => setHoverFollowButton(false)}
|
||||
onClick={() => unfollow()}
|
||||
@@ -249,7 +249,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Match>
|
||||
<Match when={!following()}>
|
||||
<button
|
||||
class="w-24 rounded-full border border-primary px-4 py-2 text-primary
|
||||
class="w-28 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
onClick={() => follow()}
|
||||
disabled={updateContactsMutation.isLoading}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
|
||||
import SafeLink from '@/components/utils/SafeLink';
|
||||
import { fixUrl } from '@/utils/imageUrl';
|
||||
|
||||
import SafeLink from '../utils/SafeLink';
|
||||
|
||||
type ImageDisplayProps = {
|
||||
url: string;
|
||||
initialHidden: boolean;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Show } from 'solid-js';
|
||||
|
||||
import EventLink from '@/components/EventLink';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import { type MentionedEvent } from '@/nostr/parseTextNote';
|
||||
|
||||
import EventLink from '../EventLink';
|
||||
|
||||
export type MentionedEventDisplayProps = {
|
||||
mentionedEvent: MentionedEvent;
|
||||
};
|
||||
@@ -14,7 +13,7 @@ const MentionedEventDisplay = (props: MentionedEventDisplayProps) => {
|
||||
return (
|
||||
<Show
|
||||
when={props.mentionedEvent.marker != null && props.mentionedEvent.marker.length > 0}
|
||||
fallback={() => <EventLink eventId={props.mentionedEvent.eventId} />}
|
||||
fallback={<EventLink eventId={props.mentionedEvent.eventId} />}
|
||||
>
|
||||
<div class="my-1 rounded border p-1">
|
||||
<TextNoteDisplayById
|
||||
|
||||
@@ -11,11 +11,10 @@ import SafeLink from '@/components/utils/SafeLink';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
|
||||
import { isImageUrl } from '@/utils/imageUrl';
|
||||
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import { isImageUrl } from '@/utils/imageUrl';
|
||||
|
||||
export type TextNoteContentDisplayProps = {
|
||||
event: NostrEvent;
|
||||
embedding: boolean;
|
||||
@@ -47,7 +46,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (item.data.type === 'note' && props.embedding) {
|
||||
return (
|
||||
<div class="my-1 rounded border p-1">
|
||||
<TextNoteDisplayById eventId={item.data.data} actions={false} />
|
||||
<TextNoteDisplayById eventId={item.data.data} actions={false} embedding={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import HeartOutlined from 'heroicons/24/outline/heart.svg';
|
||||
import HeartSolid from 'heroicons/24/solid/heart.svg';
|
||||
import { nip19, type Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import ContextMenu, { MenuItem } from '@/components/ContextMenu';
|
||||
import NotePostForm from '@/components/NotePostForm';
|
||||
import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay';
|
||||
import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay';
|
||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||
import { useTimelineContext } from '@/components/TimelineContext';
|
||||
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
@@ -29,8 +30,6 @@ import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||
import timeout from '@/utils/timeout';
|
||||
|
||||
import ContextMenu, { MenuItem } from '../ContextMenu';
|
||||
|
||||
export type TextNoteDisplayProps = {
|
||||
event: NostrEvent;
|
||||
embedding?: boolean;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Switch, Match, type Component } from 'solid-js';
|
||||
|
||||
import EventLink from '@/components/EventLink';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import TextNoteDisplay, { type TextNoteDisplayProps } from '@/components/textNote/TextNoteDisplay';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useEvent from '@/nostr/useEvent';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
|
||||
import EventLink from '../EventLink';
|
||||
|
||||
type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & {
|
||||
eventId: string | undefined;
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Switch, Match, type Component } from 'solid-js';
|
||||
import uniq from 'lodash/uniq';
|
||||
import { Filter, Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import Timeline from '@/components/Timeline';
|
||||
import { type TimelineContent } from '@/components/TimelineContext';
|
||||
import Timeline from '@/components/timeline/Timeline';
|
||||
import { type TimelineContent } from '@/components/timeline/TimelineContext';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import eventWrapper from '@/nostr/event';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
Reference in New Issue
Block a user