mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +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 parsedContent = () => parseChannelMeta(props.event.content);
|
||||
|
||||
// useChannelMeta
|
||||
|
||||
return (
|
||||
<Show when={parsedContent()} keyed>
|
||||
{(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 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 TextNoteContentDisplay from '@/components/event/textNote/TextNoteContentDisplay';
|
||||
import NotePostForm from '@/components/NotePostForm';
|
||||
import Post from '@/components/Post';
|
||||
import { useTimelineContext } from '@/components/timeline/TimelineContext';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import useFormatDate from '@/hooks/useFormatDate';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import { textNote } from '@/nostr/event';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
@@ -85,10 +85,7 @@ const EmojiReactions: Component<EmojiReactionsProps> = (props) => {
|
||||
};
|
||||
|
||||
const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
let contentRef: HTMLDivElement | undefined;
|
||||
|
||||
const { config } = useConfig();
|
||||
const formatDate = useFormatDate();
|
||||
const pubkey = usePubkey();
|
||||
const { showProfile } = useModalState();
|
||||
const timelineContext = useTimelineContext();
|
||||
@@ -97,8 +94,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
const [reposted, setReposted] = createSignal(false);
|
||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||
const closeReplyForm = () => setShowReplyForm(false);
|
||||
const [showOverflow, setShowOverflow] = createSignal(false);
|
||||
const [overflow, setOverflow] = createSignal(false);
|
||||
|
||||
const event = createMemo(() => textNote(props.event));
|
||||
|
||||
@@ -252,8 +247,6 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createdAt = () => formatDate(event().createdAtAsDate());
|
||||
|
||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -296,35 +289,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
doReaction();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (contentRef != null) {
|
||||
setOverflow(contentRef.scrollHeight > contentRef.clientHeight);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="nostr-textnote flex flex-col">
|
||||
<div class="flex w-full gap-1">
|
||||
<button
|
||||
class="author-icon h-10 w-10 shrink-0 overflow-hidden"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
showProfile(event().pubkey);
|
||||
}}
|
||||
>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<div class="nostr-textnote">
|
||||
<Post
|
||||
author={
|
||||
<span class="author flex min-w-0 truncate hover:text-blue-500">
|
||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="author-name truncate pr-1 font-bold hover:underline">
|
||||
{author()?.display_name}
|
||||
@@ -339,29 +308,12 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</Show>
|
||||
{/* TODO <Match when={author()?.nip05 != null}>@{author()?.nip05}</Match> */}
|
||||
</div>
|
||||
</button>
|
||||
<div class="created-at shrink-0">
|
||||
<a
|
||||
href={`nostr:${noteEncode(event().id)}`}
|
||||
type="button"
|
||||
class="hover:underline"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
timelineContext?.setTimeline({
|
||||
type: 'Replies',
|
||||
event: props.event,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{createdAt()}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
class="overflow-hidden"
|
||||
classList={{ 'max-h-screen': !showOverflow() }}
|
||||
>
|
||||
</span>
|
||||
}
|
||||
authorPictureUrl={author()?.picture}
|
||||
createdAt={event().createdAtAsDate()}
|
||||
content={
|
||||
<div class="textnote-content">
|
||||
<Show when={showReplyEvent()} keyed>
|
||||
{(id) => (
|
||||
<div class="mt-1 rounded border p-1">
|
||||
@@ -393,19 +345,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</div>
|
||||
</ContentWarningDisplay>
|
||||
</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>
|
||||
}
|
||||
actions={
|
||||
<Show when={actions()}>
|
||||
<Show when={config().showEmojiReaction && reactions().length > 0}>
|
||||
<EmojiReactions
|
||||
@@ -499,8 +440,8 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<Show when={showReplyForm()}>
|
||||
<NotePostForm
|
||||
mode="reply"
|
||||
@@ -509,6 +450,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
||||
onPost={closeReplyForm}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
onShowProfile={() => {
|
||||
showProfile(event().pubkey);
|
||||
}}
|
||||
onShowEvent={() => {
|
||||
timelineContext?.setTimeline({ type: 'Replies', event: props.event });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
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 Heart from 'heroicons/24/outline/heart.svg';
|
||||
import Home from 'heroicons/24/outline/home.svg';
|
||||
@@ -103,6 +104,17 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
</span>
|
||||
日本リレー
|
||||
</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
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
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 EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
||||
@@ -66,9 +66,37 @@ const RelayConfig = () => {
|
||||
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 (
|
||||
<>
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">リレー</h3>
|
||||
<p class="py-1">{config().relayUrls.length} 個のリレーが設定されています</p>
|
||||
<ul>
|
||||
<For each={config().relayUrls}>
|
||||
{(relayUrl: string) => {
|
||||
@@ -97,6 +125,22 @@ const RelayConfig = () => {
|
||||
</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('インポートに失敗しました');
|
||||
});
|
||||
}}
|
||||
>
|
||||
拡張機能からインポート
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import BasicModal from '@/components/modal/BasicModal';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { Profile } from '@/nostr/event/Profile';
|
||||
import useCommands from '@/nostr/useCommands';
|
||||
import { useProfile } from '@/nostr/useProfile';
|
||||
import useProfile from '@/nostr/useProfile';
|
||||
import usePubkey from '@/nostr/usePubkey';
|
||||
import ensureNonNull from '@/utils/ensureNonNull';
|
||||
import timeout from '@/utils/timeout';
|
||||
|
||||
@@ -77,6 +77,12 @@ export type ReactionsColumnType = BaseColumn & {
|
||||
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 */
|
||||
export type RelaysColumnType = BaseColumn & {
|
||||
columnType: 'Relays';
|
||||
@@ -98,9 +104,10 @@ export type CustomFilterColumnType = BaseColumn & {
|
||||
export type ColumnType =
|
||||
| FollowingColumnType
|
||||
| NotificationColumnType
|
||||
| RelaysColumnType
|
||||
| PostsColumnType
|
||||
| ReactionsColumnType
|
||||
| ChannelColumnType
|
||||
| RelaysColumnType
|
||||
| SearchColumnType
|
||||
| CustomFilterColumnType;
|
||||
|
||||
@@ -159,6 +166,14 @@ export const createReactionsColumn = (
|
||||
...params,
|
||||
});
|
||||
|
||||
export const createChannelColumn = (
|
||||
params: CreateParams<ChannelColumnType>,
|
||||
): ChannelColumnType => ({
|
||||
...createBaseColumn(),
|
||||
columnType: 'Channel',
|
||||
...params,
|
||||
});
|
||||
|
||||
export const createSearchColumn = (params: CreateParams<SearchColumnType>): SearchColumnType => ({
|
||||
...createBaseColumn(),
|
||||
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);
|
||||
};
|
||||
|
||||
export const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
|
||||
const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
|
||||
const queryClient = useQueryClient();
|
||||
const props = createMemo(propsProvider);
|
||||
const genQueryKey = createMemo((): UseProfileQueryKey => ['useProfile', props()] as const);
|
||||
|
||||
@@ -17,7 +17,7 @@ export type UseReposts = {
|
||||
query: CreateQueryResult<NostrEvent[]>;
|
||||
};
|
||||
|
||||
export const useReposts = (propsProvider: () => UseRepostsProps): UseReposts => {
|
||||
const useReposts = (propsProvider: () => UseRepostsProps): UseReposts => {
|
||||
const queryClient = useQueryClient();
|
||||
const props = createMemo(propsProvider);
|
||||
const genQueryKey = createMemo(() => ['useReposts', props()] as const);
|
||||
|
||||
@@ -23,10 +23,14 @@ const Home: Component = () => {
|
||||
|
||||
createEffect(() => {
|
||||
config().relayUrls.map(async (relayUrl) => {
|
||||
try {
|
||||
const relay = await pool().ensureRelay(relayUrl);
|
||||
relay.on('notice', (msg: string) => {
|
||||
console.error(`NOTICE: ${relayUrl}: ${msg}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('ensureRelay failed', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user