mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 22:14:26 +01:00
update
This commit is contained in:
92
src/components/Post.tsx
Normal file
92
src/components/Post.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Component, JSX, Show, createSignal } from 'solid-js';
|
||||||
|
|
||||||
|
import useDetectOverflow from '@/hooks/useDetectOverflow';
|
||||||
|
import useFormatDate from '@/hooks/useFormatDate';
|
||||||
|
|
||||||
|
export type PostProps = {
|
||||||
|
author: JSX.Element;
|
||||||
|
createdAt: Date;
|
||||||
|
content: JSX.Element;
|
||||||
|
actions?: JSX.Element;
|
||||||
|
footer?: JSX.Element;
|
||||||
|
authorPictureUrl?: string;
|
||||||
|
onShowProfile?: () => void;
|
||||||
|
onShowEvent?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Post: Component<PostProps> = (props) => {
|
||||||
|
const { overflow, elementRef } = useDetectOverflow();
|
||||||
|
const formatDate = useFormatDate();
|
||||||
|
|
||||||
|
const [showOverflow, setShowOverflow] = createSignal();
|
||||||
|
const createdAt = () => formatDate(props.createdAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="post flex flex-col">
|
||||||
|
<div class="flex w-full gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="author-icon h-10 w-10 shrink-0 overflow-hidden"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
props.onShowProfile?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={props.authorPictureUrl} keyed>
|
||||||
|
{(url) => <img src={url} alt="icon" class="h-full w-full rounded object-cover" />}
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
<div class="min-w-0 flex-auto">
|
||||||
|
<div class="flex justify-between gap-1 text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="author flex min-w-0 truncate hover:text-blue-500"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
props?.onShowProfile?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.author}
|
||||||
|
</button>
|
||||||
|
<div class="created-at shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hover:underline"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
props.onShowEvent?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{createdAt()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={elementRef}
|
||||||
|
class="overflow-hidden"
|
||||||
|
classList={{ 'max-h-screen': !showOverflow() }}
|
||||||
|
>
|
||||||
|
{props.content}
|
||||||
|
</div>
|
||||||
|
<Show when={overflow()}>
|
||||||
|
<button
|
||||||
|
class="mt-2 w-full rounded border p-2 text-center text-xs text-stone-600 shadow-sm hover:shadow"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setShowOverflow((current) => !current);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!showOverflow()} fallback="隠す">
|
||||||
|
続きを読む
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<div class="actions">{props.actions}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={props.footer}>{props.footer}</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Post;
|
||||||
62
src/components/column/ChannelColumn.tsx
Normal file
62
src/components/column/ChannelColumn.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Component, createEffect, onCleanup, onMount } from 'solid-js';
|
||||||
|
|
||||||
|
import ChatBubbleLeftRight from 'heroicons/24/outline/chat-bubble-left-right.svg';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import { Kind } from 'nostr-tools';
|
||||||
|
|
||||||
|
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 { ChannelColumnType, FollowingColumnType } from '@/core/column';
|
||||||
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
|
import useConfig from '@/core/useConfig';
|
||||||
|
import useFollowings from '@/nostr/useFollowings';
|
||||||
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
|
import epoch from '@/utils/epoch';
|
||||||
|
|
||||||
|
export type ChannelColumnProps = {
|
||||||
|
columnIndex: number;
|
||||||
|
lastColumn: boolean;
|
||||||
|
column: ChannelColumnType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChannelColumn: Component<ChannelColumnProps> = (props) => {
|
||||||
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
|
const { events } = useSubscription(() => ({
|
||||||
|
relayUrls: config().relayUrls,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
kinds: [Kind.ChannelMessage],
|
||||||
|
limit: 10,
|
||||||
|
'#e': [props.column.id],
|
||||||
|
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={<ChatBubbleLeftRight />}
|
||||||
|
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 ChannelColumn;
|
||||||
0
src/components/event/ChannelMessage.tsx
Normal file
0
src/components/event/ChannelMessage.tsx
Normal file
@@ -12,6 +12,8 @@ export type ChannelInfoProps = {
|
|||||||
const ChannelInfo: Component<ChannelInfoProps> = (props) => {
|
const ChannelInfo: Component<ChannelInfoProps> = (props) => {
|
||||||
const parsedContent = () => parseChannelMeta(props.event.content);
|
const parsedContent = () => parseChannelMeta(props.event.content);
|
||||||
|
|
||||||
|
// useChannelMeta
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={parsedContent()} keyed>
|
<Show when={parsedContent()} keyed>
|
||||||
{(meta) => (
|
{(meta) => (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Show, For, createSignal, createMemo, onMount, type JSX, type Component } from 'solid-js';
|
import { Show, For, createSignal, createMemo, type JSX, type Component } from 'solid-js';
|
||||||
|
|
||||||
import { createMutation } from '@tanstack/solid-query';
|
import { createMutation } from '@tanstack/solid-query';
|
||||||
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
|
||||||
@@ -17,9 +17,9 @@ import ContentWarningDisplay from '@/components/event/textNote/ContentWarningDis
|
|||||||
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
|
import GeneralUserMentionDisplay from '@/components/event/textNote/GeneralUserMentionDisplay';
|
||||||
import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
|
import TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
|
||||||
import NotePostForm from '@/components/NotePostForm';
|
import NotePostForm from '@/components/NotePostForm';
|
||||||
|
import Post from '@/components/Post';
|
||||||
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import useFormatDate from '@/hooks/useFormatDate';
|
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
import { textNote } from '@/nostr/event';
|
import { textNote } from '@/nostr/event';
|
||||||
import useCommands from '@/nostr/useCommands';
|
import useCommands from '@/nostr/useCommands';
|
||||||
@@ -85,10 +85,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||||
let contentRef: HTMLDivElement | undefined;
|
|
||||||
|
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const formatDate = useFormatDate();
|
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile } = useModalState();
|
const { showProfile } = useModalState();
|
||||||
const timelineContext = useTimelineContext();
|
const timelineContext = useTimelineContext();
|
||||||
@@ -97,8 +94,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
const [reposted, setReposted] = createSignal(false);
|
const [reposted, setReposted] = createSignal(false);
|
||||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||||
const closeReplyForm = () => setShowReplyForm(false);
|
const closeReplyForm = () => setShowReplyForm(false);
|
||||||
const [showOverflow, setShowOverflow] = createSignal(false);
|
|
||||||
const [overflow, setOverflow] = createSignal(false);
|
|
||||||
|
|
||||||
const event = createMemo(() => textNote(props.event));
|
const event = createMemo(() => textNote(props.event));
|
||||||
|
|
||||||
@@ -252,8 +247,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createdAt = () => formatDate(event().createdAtAsDate());
|
|
||||||
|
|
||||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
@@ -296,72 +289,31 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
doReaction();
|
doReaction();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (contentRef != null) {
|
|
||||||
setOverflow(contentRef.scrollHeight > contentRef.clientHeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="nostr-textnote flex flex-col">
|
<div class="nostr-textnote">
|
||||||
<div class="flex w-full gap-1">
|
<Post
|
||||||
<button
|
author={
|
||||||
class="author-icon h-10 w-10 shrink-0 overflow-hidden"
|
<span class="author flex min-w-0 truncate hover:text-blue-500">
|
||||||
onClick={(ev) => {
|
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||||
ev.stopPropagation();
|
<div class="author-name truncate pr-1 font-bold hover:underline">
|
||||||
showProfile(event().pubkey);
|
{author()?.display_name}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={author()?.picture}>
|
|
||||||
<img src={author()?.picture} alt="icon" class="h-full w-full rounded object-cover" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
<div class="min-w-0 flex-auto">
|
|
||||||
<div class="flex justify-between gap-1 text-xs">
|
|
||||||
<button
|
|
||||||
class="author flex min-w-0 truncate hover:text-blue-500"
|
|
||||||
onClick={(ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
showProfile(event().pubkey);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
|
||||||
<div class="author-name truncate pr-1 font-bold hover:underline">
|
|
||||||
{author()?.display_name}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="author-username truncate text-zinc-600">
|
|
||||||
<Show
|
|
||||||
when={author()?.name != null}
|
|
||||||
fallback={`@${npubEncodeFallback(event().pubkey)}`}
|
|
||||||
>
|
|
||||||
@{author()?.name}
|
|
||||||
</Show>
|
|
||||||
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</Show>
|
||||||
<div class="created-at shrink-0">
|
<div class="author-username truncate text-zinc-600">
|
||||||
<a
|
<Show
|
||||||
href={`nostr:${noteEncode(event().id)}`}
|
when={author()?.name != null}
|
||||||
type="button"
|
fallback={`@${npubEncodeFallback(event().pubkey)}`}
|
||||||
class="hover:underline"
|
|
||||||
onClick={(ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
timelineContext?.setTimeline({
|
|
||||||
type: 'Replies',
|
|
||||||
event: props.event,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{createdAt()}
|
@{author()?.name}
|
||||||
</a>
|
</Show>
|
||||||
|
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</span>
|
||||||
<div
|
}
|
||||||
ref={contentRef}
|
authorPictureUrl={author()?.picture}
|
||||||
class="overflow-hidden"
|
createdAt={event().createdAtAsDate()}
|
||||||
classList={{ 'max-h-screen': !showOverflow() }}
|
content={
|
||||||
>
|
<div class="textnote-content">
|
||||||
<Show when={showReplyEvent()} keyed>
|
<Show when={showReplyEvent()} keyed>
|
||||||
{(id) => (
|
{(id) => (
|
||||||
<div class="mt-1 rounded border p-1">
|
<div class="mt-1 rounded border p-1">
|
||||||
@@ -393,19 +345,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</ContentWarningDisplay>
|
</ContentWarningDisplay>
|
||||||
</div>
|
</div>
|
||||||
<Show when={overflow()}>
|
}
|
||||||
<button
|
actions={
|
||||||
class="mt-2 w-full rounded border p-2 text-center text-xs text-stone-600 shadow-sm hover:shadow"
|
|
||||||
onClick={(ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
setShowOverflow((current) => !current);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={!showOverflow()} fallback="隠す">
|
|
||||||
続きを読む
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={actions()}>
|
<Show when={actions()}>
|
||||||
<Show when={config().showEmojiReaction && reactions().length > 0}>
|
<Show when={config().showEmojiReaction && reactions().length > 0}>
|
||||||
<EmojiReactions
|
<EmojiReactions
|
||||||
@@ -499,16 +440,24 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
}
|
||||||
</div>
|
footer={
|
||||||
<Show when={showReplyForm()}>
|
<Show when={showReplyForm()}>
|
||||||
<NotePostForm
|
<NotePostForm
|
||||||
mode="reply"
|
mode="reply"
|
||||||
replyTo={props.event}
|
replyTo={props.event}
|
||||||
onClose={closeReplyForm}
|
onClose={closeReplyForm}
|
||||||
onPost={closeReplyForm}
|
onPost={closeReplyForm}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
}
|
||||||
|
onShowProfile={() => {
|
||||||
|
showProfile(event().pubkey);
|
||||||
|
}}
|
||||||
|
onShowEvent={() => {
|
||||||
|
timelineContext?.setTimeline({ type: 'Replies', event: props.event });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component } from 'solid-js';
|
import { Component } from 'solid-js';
|
||||||
|
|
||||||
import Bell from 'heroicons/24/outline/bell.svg';
|
import Bell from 'heroicons/24/outline/bell.svg';
|
||||||
|
import ChatBubbleLeftRight from 'heroicons/24/outline/chat-bubble-left-right.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';
|
||||||
@@ -103,6 +104,17 @@ 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={() => window.alert()}
|
||||||
|
>
|
||||||
|
<span class="inline-block h-8 w-8">
|
||||||
|
<ChatBubbleLeftRight />
|
||||||
|
</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={() => addSearchColumn()}
|
onClick={() => addSearchColumn()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, Show, For, type JSX } from 'solid-js';
|
import { createSignal, Show, For, type JSX, batch } from 'solid-js';
|
||||||
|
|
||||||
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
||||||
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
||||||
@@ -66,37 +66,81 @@ const RelayConfig = () => {
|
|||||||
setRelayUrlInput('');
|
setRelayUrlInput('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importFromNIP07 = async () => {
|
||||||
|
if (window.nostr == null) return;
|
||||||
|
|
||||||
|
const importedRelays = Object.entries((await window.nostr?.getRelays?.()) ?? []);
|
||||||
|
const relayUrls = importedRelays.map(([relayUrl]) => relayUrl).join('\n');
|
||||||
|
|
||||||
|
if (importedRelays.length === 0) {
|
||||||
|
window.alert('リレーが設定されていません');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.confirm(`これらのリレーをインポートしますか:\n${relayUrls}`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCount = config().relayUrls.length;
|
||||||
|
batch(() => {
|
||||||
|
importedRelays.forEach(([relayUrl]) => {
|
||||||
|
addRelay(relayUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const currentCount = config().relayUrls.length;
|
||||||
|
const importedCount = currentCount - lastCount;
|
||||||
|
window.alert(`${importedCount} 個のリレーをインポートしました`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<>
|
||||||
<h3 class="font-bold">リレー</h3>
|
<div class="py-2">
|
||||||
<ul>
|
<h3 class="font-bold">リレー</h3>
|
||||||
<For each={config().relayUrls}>
|
<p class="py-1">{config().relayUrls.length} 個のリレーが設定されています</p>
|
||||||
{(relayUrl: string) => {
|
<ul>
|
||||||
return (
|
<For each={config().relayUrls}>
|
||||||
<li class="flex items-center">
|
{(relayUrl: string) => {
|
||||||
<div class="flex-1 truncate">{relayUrl}</div>
|
return (
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeRelay(relayUrl)}>
|
<li class="flex items-center">
|
||||||
<XMark />
|
<div class="flex-1 truncate">{relayUrl}</div>
|
||||||
</button>
|
<button class="h-3 w-3 shrink-0" onClick={() => removeRelay(relayUrl)}>
|
||||||
</li>
|
<XMark />
|
||||||
);
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
<form class="flex gap-2" onSubmit={handleClickAddRelay}>
|
||||||
|
<input
|
||||||
|
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||||
|
type="text"
|
||||||
|
name="relayUrl"
|
||||||
|
value={relayUrlInput()}
|
||||||
|
pattern={RelayUrlRegex}
|
||||||
|
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||||
|
追加
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="py-2">
|
||||||
|
<h3 class="pb-1 font-bold">インポート</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded bg-rose-300 p-2 font-bold text-white"
|
||||||
|
onClick={() => {
|
||||||
|
importFromNIP07().catch((err) => {
|
||||||
|
console.error('failed to import relays', err);
|
||||||
|
window.alert('インポートに失敗しました');
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
</For>
|
>
|
||||||
</ul>
|
拡張機能からインポート
|
||||||
<form class="flex gap-2" onSubmit={handleClickAddRelay}>
|
|
||||||
<input
|
|
||||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
|
||||||
type="text"
|
|
||||||
name="relayUrl"
|
|
||||||
value={relayUrlInput()}
|
|
||||||
pattern={RelayUrlRegex}
|
|
||||||
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
|
||||||
追加
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import BasicModal from '@/components/modal/BasicModal';
|
|||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { Profile } from '@/nostr/event/Profile';
|
import { Profile } from '@/nostr/event/Profile';
|
||||||
import useCommands from '@/nostr/useCommands';
|
import useCommands from '@/nostr/useCommands';
|
||||||
import { useProfile } from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
import ensureNonNull from '@/utils/ensureNonNull';
|
import ensureNonNull from '@/utils/ensureNonNull';
|
||||||
import timeout from '@/utils/timeout';
|
import timeout from '@/utils/timeout';
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ export type ReactionsColumnType = BaseColumn & {
|
|||||||
pubkey: string;
|
pubkey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A column which shows reactions published by the specific user */
|
||||||
|
export type ChannelColumnType = BaseColumn & {
|
||||||
|
columnType: 'Channel';
|
||||||
|
rootEventId: string;
|
||||||
|
};
|
||||||
|
|
||||||
/** A column which shows text notes and reposts posted to the specific relays */
|
/** A column which shows text notes and reposts posted to the specific relays */
|
||||||
export type RelaysColumnType = BaseColumn & {
|
export type RelaysColumnType = BaseColumn & {
|
||||||
columnType: 'Relays';
|
columnType: 'Relays';
|
||||||
@@ -98,9 +104,10 @@ export type CustomFilterColumnType = BaseColumn & {
|
|||||||
export type ColumnType =
|
export type ColumnType =
|
||||||
| FollowingColumnType
|
| FollowingColumnType
|
||||||
| NotificationColumnType
|
| NotificationColumnType
|
||||||
| RelaysColumnType
|
|
||||||
| PostsColumnType
|
| PostsColumnType
|
||||||
| ReactionsColumnType
|
| ReactionsColumnType
|
||||||
|
| ChannelColumnType
|
||||||
|
| RelaysColumnType
|
||||||
| SearchColumnType
|
| SearchColumnType
|
||||||
| CustomFilterColumnType;
|
| CustomFilterColumnType;
|
||||||
|
|
||||||
@@ -159,6 +166,14 @@ export const createReactionsColumn = (
|
|||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createChannelColumn = (
|
||||||
|
params: CreateParams<ChannelColumnType>,
|
||||||
|
): ChannelColumnType => ({
|
||||||
|
...createBaseColumn(),
|
||||||
|
columnType: 'Channel',
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
export const createSearchColumn = (params: CreateParams<SearchColumnType>): SearchColumnType => ({
|
export const createSearchColumn = (params: CreateParams<SearchColumnType>): SearchColumnType => ({
|
||||||
...createBaseColumn(),
|
...createBaseColumn(),
|
||||||
columnType: 'Search',
|
columnType: 'Search',
|
||||||
|
|||||||
20
src/hooks/useDetectOverflow.ts
Normal file
20
src/hooks/useDetectOverflow.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createSignal, onMount } from 'solid-js';
|
||||||
|
|
||||||
|
const useDetectOverflow = () => {
|
||||||
|
let elementRef: HTMLElement | undefined;
|
||||||
|
const [overflow, setOverflow] = createSignal(false);
|
||||||
|
|
||||||
|
const setElementRef = (el: HTMLElement) => {
|
||||||
|
elementRef = el;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (elementRef != null) {
|
||||||
|
setOverflow(elementRef.scrollHeight > elementRef.clientHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { overflow, elementRef: setElementRef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDetectOverflow;
|
||||||
0
src/nostr/useChannelMeta.ts
Normal file
0
src/nostr/useChannelMeta.ts
Normal file
@@ -74,7 +74,7 @@ const getProfile = ({
|
|||||||
return timeout(15000, `useProfile: ${pubkey}`)(promise);
|
return timeout(15000, `useProfile: ${pubkey}`)(promise);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
|
const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const props = createMemo(propsProvider);
|
const props = createMemo(propsProvider);
|
||||||
const genQueryKey = createMemo((): UseProfileQueryKey => ['useProfile', props()] as const);
|
const genQueryKey = createMemo((): UseProfileQueryKey => ['useProfile', props()] as const);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type UseReposts = {
|
|||||||
query: CreateQueryResult<NostrEvent[]>;
|
query: CreateQueryResult<NostrEvent[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useReposts = (propsProvider: () => UseRepostsProps): UseReposts => {
|
const useReposts = (propsProvider: () => UseRepostsProps): UseReposts => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const props = createMemo(propsProvider);
|
const props = createMemo(propsProvider);
|
||||||
const genQueryKey = createMemo(() => ['useReposts', props()] as const);
|
const genQueryKey = createMemo(() => ['useReposts', props()] as const);
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ const Home: Component = () => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
config().relayUrls.map(async (relayUrl) => {
|
config().relayUrls.map(async (relayUrl) => {
|
||||||
const relay = await pool().ensureRelay(relayUrl);
|
try {
|
||||||
relay.on('notice', (msg: string) => {
|
const relay = await pool().ensureRelay(relayUrl);
|
||||||
console.error(`NOTICE: ${relayUrl}: ${msg}`);
|
relay.on('notice', (msg: string) => {
|
||||||
});
|
console.error(`NOTICE: ${relayUrl}: ${msg}`);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('ensureRelay failed', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user