feat: column customization

This commit is contained in:
Shusui MOYATANI
2023-05-08 19:58:59 +09:00
parent 5b5c261285
commit f8fbc95ba7
49 changed files with 1178 additions and 307 deletions

View File

@@ -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;
};

View File

@@ -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}

View File

@@ -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

View File

@@ -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) => {

View 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;

View File

@@ -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()}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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>
);
};

View File

@@ -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"

View 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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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';