mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
feat: support search column
This commit is contained in:
@@ -29,7 +29,7 @@ const ColumnSettingsSection: Component<ColumnSettingsSectionProps> = (props) =>
|
|||||||
|
|
||||||
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
||||||
const { saveColumn, removeColumn, moveColumn } = useConfig();
|
const { saveColumn, removeColumn, moveColumn } = useConfig();
|
||||||
const requestCommand = useRequestCommand();
|
const request = useRequestCommand();
|
||||||
|
|
||||||
const setColumnWidth = (width: ColumnType['width']) => {
|
const setColumnWidth = (width: ColumnType['width']) => {
|
||||||
saveColumn({ ...props.column, width });
|
saveColumn({ ...props.column, width });
|
||||||
@@ -37,9 +37,7 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
|||||||
|
|
||||||
const move = (index: number) => {
|
const move = (index: number) => {
|
||||||
moveColumn(props.column.id, index);
|
moveColumn(props.column.id, index);
|
||||||
requestCommand({ command: 'moveToColumn', columnIndex: index }).catch((err) =>
|
request({ command: 'moveToColumn', columnIndex: index }).catch((err) => console.error(err));
|
||||||
console.error(err),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component } from 'solid-js';
|
import { Component, createSignal, Show, JSX, onMount } from 'solid-js';
|
||||||
|
|
||||||
|
import EllipsisVertical from 'heroicons/24/outline/ellipsis-vertical.svg';
|
||||||
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
||||||
|
|
||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
import Timeline from '@/components/timeline/Timeline';
|
import Timeline from '@/components/timeline/Timeline';
|
||||||
@@ -12,7 +12,60 @@ import { relaysForSearching } from '@/core/relayUrls';
|
|||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
|
||||||
type SearchColumnDisplayProps = {
|
export type SearchColumnHeaderProps = {
|
||||||
|
name: string;
|
||||||
|
column: SearchColumnType;
|
||||||
|
settings: () => JSX.Element;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchColumnHeader: Component<SearchColumnHeaderProps> = (props) => {
|
||||||
|
const [isSettingsOpened, setIsSettingOpened] = createSignal(false);
|
||||||
|
const [query, setQuery] = createSignal('');
|
||||||
|
|
||||||
|
const { saveColumn } = useConfig();
|
||||||
|
|
||||||
|
const toggleSettingsOpened = () => setIsSettingOpened((current) => !current);
|
||||||
|
|
||||||
|
const handleBlur: JSX.EventHandler<HTMLInputElement, Event> = () => {
|
||||||
|
if (query() === props.column.query) return;
|
||||||
|
saveColumn({ ...props.column, query: query() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange: JSX.EventHandler<HTMLInputElement, Event> = (ev) => {
|
||||||
|
setQuery(ev.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setQuery(props.column.query);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex h-8 items-center gap-1 px-2">
|
||||||
|
<h2 class="flex items-center gap-1">
|
||||||
|
<span class="inline-block h-4 w-4 text-gray-700">
|
||||||
|
<MagnifyingGlass />
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<input
|
||||||
|
class="flex-1 rounded border border-stone-300 px-1 py-0 focus:border-rose-100 focus:ring-rose-300"
|
||||||
|
type="text"
|
||||||
|
name="query"
|
||||||
|
value={query()}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
<button class="h-4 w-4" onClick={() => toggleSettingsOpened()}>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show when={isSettingsOpened()}>{props.settings()}</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchColumnDisplayProps = {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
lastColumn: boolean;
|
lastColumn: boolean;
|
||||||
column: SearchColumnType;
|
column: SearchColumnType;
|
||||||
@@ -21,27 +74,33 @@ type SearchColumnDisplayProps = {
|
|||||||
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||||
const { removeColumn } = useConfig();
|
const { removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const { events } = useSubscription(() => {
|
||||||
relayUrls: relaysForSearching,
|
const { query } = props.column;
|
||||||
filters: [
|
|
||||||
{
|
if (query.length === 0) return null;
|
||||||
kinds: [1, 6],
|
|
||||||
search: props.column.query,
|
return {
|
||||||
limit: 25,
|
relayUrls: relaysForSearching,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
kinds: [1, 6],
|
||||||
|
search: query,
|
||||||
|
limit: 25,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
clientEventFilter: (event) => {
|
||||||
|
if (props.column.contentFilter == null) return true;
|
||||||
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
clientEventFilter: (event) => {
|
});
|
||||||
if (props.column.contentFilter == null) return true;
|
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
<BasicColumnHeader
|
<SearchColumnHeader
|
||||||
name={props.column.name ?? '検索'}
|
name={props.column.name ?? '検索'}
|
||||||
icon={<MagnifyingGlass />}
|
column={props.column}
|
||||||
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
|
||||||
onClose={() => removeColumn(props.column.id)}
|
onClose={() => removeColumn(props.column.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Bell from 'heroicons/24/outline/bell.svg';
|
|||||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
import GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||||
import Heart from 'heroicons/24/outline/heart.svg';
|
import Heart from 'heroicons/24/outline/heart.svg';
|
||||||
import Home from 'heroicons/24/outline/home.svg';
|
import Home from 'heroicons/24/outline/home.svg';
|
||||||
|
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
||||||
import User from 'heroicons/24/outline/user.svg';
|
import User from 'heroicons/24/outline/user.svg';
|
||||||
|
|
||||||
import BasicModal from '@/components/modal/BasicModal';
|
import BasicModal from '@/components/modal/BasicModal';
|
||||||
@@ -13,8 +14,10 @@ import {
|
|||||||
createNotificationColumn,
|
createNotificationColumn,
|
||||||
createPostsColumn,
|
createPostsColumn,
|
||||||
createReactionsColumn,
|
createReactionsColumn,
|
||||||
|
createSearchColumn,
|
||||||
} from '@/core/column';
|
} from '@/core/column';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useRequestCommand } from '@/hooks/useCommandBus';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
|
|
||||||
@@ -25,38 +28,49 @@ type AddColumnProps = {
|
|||||||
const AddColumn: Component<AddColumnProps> = (props) => {
|
const AddColumn: Component<AddColumnProps> = (props) => {
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { saveColumn } = useConfig();
|
const { saveColumn } = useConfig();
|
||||||
|
const request = useRequestCommand();
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
props.onClose();
|
||||||
|
request({ command: 'moveToLastColumn' }).catch((err) => console.error(err));
|
||||||
|
};
|
||||||
|
|
||||||
const addFollowingColumn = () => {
|
const addFollowingColumn = () => {
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||||
saveColumn(createFollowingColumn({ pubkey: pubkeyNonNull }));
|
saveColumn(createFollowingColumn({ pubkey: pubkeyNonNull }));
|
||||||
});
|
});
|
||||||
props.onClose();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNotificationColumn = () => {
|
const addNotificationColumn = () => {
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||||
saveColumn(createNotificationColumn({ pubkey: pubkeyNonNull }));
|
saveColumn(createNotificationColumn({ pubkey: pubkeyNonNull }));
|
||||||
});
|
});
|
||||||
props.onClose();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addJapanRelaysColumn = () => {
|
const addJapanRelaysColumn = () => {
|
||||||
saveColumn(createJapanRelaysColumn());
|
saveColumn(createJapanRelaysColumn());
|
||||||
props.onClose();
|
finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSearchColumn = () => {
|
||||||
|
saveColumn(createSearchColumn({ query: '' }));
|
||||||
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addMyPostsColumn = () => {
|
const addMyPostsColumn = () => {
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||||
saveColumn(createPostsColumn({ pubkey: pubkeyNonNull }));
|
saveColumn(createPostsColumn({ pubkey: pubkeyNonNull }));
|
||||||
});
|
});
|
||||||
props.onClose();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const addMyReactionsColumn = () => {
|
const addMyReactionsColumn = () => {
|
||||||
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => {
|
||||||
saveColumn(createReactionsColumn({ pubkey: pubkeyNonNull }));
|
saveColumn(createReactionsColumn({ pubkey: pubkeyNonNull }));
|
||||||
});
|
});
|
||||||
props.onClose();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,6 +103,15 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
日本リレー
|
日本リレー
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||||
|
onClick={() => addSearchColumn()}
|
||||||
|
>
|
||||||
|
<span class="inline-block h-8 w-8">
|
||||||
|
<MagnifyingGlass />
|
||||||
|
</span>
|
||||||
|
検索
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||||
onClick={() => addMyPostsColumn()}
|
onClick={() => addMyPostsColumn()}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
|||||||
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
||||||
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById';
|
||||||
import SafeLink from '@/components/utils/SafeLink';
|
import SafeLink from '@/components/utils/SafeLink';
|
||||||
|
import { createSearchColumn } from '@/core/column';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
|
import { useRequestCommand } from '@/hooks/useCommandBus';
|
||||||
import eventWrapper from '@/nostr/event';
|
import eventWrapper from '@/nostr/event';
|
||||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
|
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/nostr/parseTextNote';
|
||||||
import { isImageUrl } from '@/utils/imageUrl';
|
import { isImageUrl } from '@/utils/imageUrl';
|
||||||
@@ -21,8 +23,17 @@ export type TextNoteContentDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||||
const { config } = useConfig();
|
const { config, saveColumn } = useConfig();
|
||||||
|
|
||||||
|
const request = useRequestCommand();
|
||||||
|
|
||||||
const event = () => eventWrapper(props.event);
|
const event = () => eventWrapper(props.event);
|
||||||
|
|
||||||
|
const addHashTagColumn = (query: string) => {
|
||||||
|
saveColumn(createSearchColumn({ query }));
|
||||||
|
request({ command: 'moveToLastColumn' }).catch((err) => console.error(err));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<For each={parseTextNote(props.event.content)}>
|
<For each={parseTextNote(props.event.content)}>
|
||||||
{(item: ParsedTextNoteNode) => {
|
{(item: ParsedTextNoteNode) => {
|
||||||
@@ -59,7 +70,11 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
|||||||
return <span class="text-blue-500 underline">{item.content}</span>;
|
return <span class="text-blue-500 underline">{item.content}</span>;
|
||||||
}
|
}
|
||||||
if (item.type === 'HashTag') {
|
if (item.type === 'HashTag') {
|
||||||
return <span class="text-blue-500 underline">{item.content}</span>;
|
return (
|
||||||
|
<button class="text-blue-500 underline" onClick={() => addHashTagColumn(item.content)}>
|
||||||
|
{item.content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (item.type === 'URL') {
|
if (item.type === 'URL') {
|
||||||
if (isImageUrl(item.content)) {
|
if (isImageUrl(item.content)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user