diff --git a/src/components/Post.tsx b/src/components/Post.tsx new file mode 100644 index 0000000..cf96c53 --- /dev/null +++ b/src/components/Post.tsx @@ -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 = (props) => { + const { overflow, elementRef } = useDetectOverflow(); + const formatDate = useFormatDate(); + + const [showOverflow, setShowOverflow] = createSignal(); + const createdAt = () => formatDate(props.createdAt); + + return ( +
+
+ +
+
+ +
+ +
+
+
+ {props.content} +
+ + + +
{props.actions}
+
+
+ {props.footer} +
+ ); +}; + +export default Post; diff --git a/src/components/column/ChannelColumn.tsx b/src/components/column/ChannelColumn.tsx new file mode 100644 index 0000000..ea8cff2 --- /dev/null +++ b/src/components/column/ChannelColumn.tsx @@ -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 = (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 ( + } + settings={() => } + onClose={() => removeColumn(props.column.id)} + /> + } + width={props.column.width} + columnIndex={props.columnIndex} + lastColumn={props.lastColumn} + > + + + ); +}; + +export default ChannelColumn; diff --git a/src/components/event/ChannelMessage.tsx b/src/components/event/ChannelMessage.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/event/ChannelMetaDisplay.tsx b/src/components/event/ChannelMetaDisplay.tsx index e539196..f9738de 100644 --- a/src/components/event/ChannelMetaDisplay.tsx +++ b/src/components/event/ChannelMetaDisplay.tsx @@ -12,6 +12,8 @@ export type ChannelInfoProps = { const ChannelInfo: Component = (props) => { const parsedContent = () => parseChannelMeta(props.event.content); + // useChannelMeta + return ( {(meta) => ( diff --git a/src/components/event/textNote/TextNoteDisplay.tsx b/src/components/event/textNote/TextNoteDisplay.tsx index ab7a3d6..820bf02 100644 --- a/src/components/event/textNote/TextNoteDisplay.tsx +++ b/src/components/event/textNote/TextNoteDisplay.tsx @@ -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 = (props) => { }; const TextNoteDisplay: Component = (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 = (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 = (props) => { return undefined; }; - const createdAt = () => formatDate(event().createdAtAsDate()); - const handleRepost: JSX.EventHandler = (ev) => { ev.stopPropagation(); @@ -296,72 +289,31 @@ const TextNoteDisplay: Component = (props) => { doReaction(); }; - onMount(() => { - if (contentRef != null) { - setOverflow(contentRef.scrollHeight > contentRef.clientHeight); - } - }); - return ( -
-
- -
-
- - -
+ + } + authorPictureUrl={author()?.picture} + createdAt={event().createdAtAsDate()} + content={ +
{(id) => (
@@ -393,19 +345,8 @@ const TextNoteDisplay: Component = (props) => {
- - - + } + actions={ 0}> = (props) => {
-
-
- - - + } + footer={ + + + + } + onShowProfile={() => { + showProfile(event().pubkey); + }} + onShowEvent={() => { + timelineContext?.setTimeline({ type: 'Replies', event: props.event }); + }} + />
); }; diff --git a/src/components/modal/AddColumn.tsx b/src/components/modal/AddColumn.tsx index 63cec62..94ebd76 100644 --- a/src/components/modal/AddColumn.tsx +++ b/src/components/modal/AddColumn.tsx @@ -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 = (props) => { 日本リレー + {/* + + */} - - ); + <> +
+

リレー

+

{config().relayUrls.length} 個のリレーが設定されています

+
    + + {(relayUrl: string) => { + return ( +
  • +
    {relayUrl}
    + +
  • + ); + }} +
    +
+
+ setRelayUrlInput(ev.currentTarget.value)} + /> + +
+
+
+

インポート

+ - -
+ + ); }; diff --git a/src/components/modal/ProfileEdit.tsx b/src/components/modal/ProfileEdit.tsx index e7aee71..a167600 100644 --- a/src/components/modal/ProfileEdit.tsx +++ b/src/components/modal/ProfileEdit.tsx @@ -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'; diff --git a/src/core/column.ts b/src/core/column.ts index ef6db4c..df9090c 100644 --- a/src/core/column.ts +++ b/src/core/column.ts @@ -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 => ({ + ...createBaseColumn(), + columnType: 'Channel', + ...params, +}); + export const createSearchColumn = (params: CreateParams): SearchColumnType => ({ ...createBaseColumn(), columnType: 'Search', diff --git a/src/hooks/useDetectOverflow.ts b/src/hooks/useDetectOverflow.ts new file mode 100644 index 0000000..2b30d64 --- /dev/null +++ b/src/hooks/useDetectOverflow.ts @@ -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; diff --git a/src/nostr/useChannelMeta.ts b/src/nostr/useChannelMeta.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/nostr/useProfile.ts b/src/nostr/useProfile.ts index b515f33..ff00d54 100644 --- a/src/nostr/useProfile.ts +++ b/src/nostr/useProfile.ts @@ -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); diff --git a/src/nostr/useReposts.ts b/src/nostr/useReposts.ts index 78aaed7..dacea3b 100644 --- a/src/nostr/useReposts.ts +++ b/src/nostr/useReposts.ts @@ -17,7 +17,7 @@ export type UseReposts = { query: CreateQueryResult; }; -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); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 08c6c76..5ab3cd1 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -23,10 +23,14 @@ const Home: Component = () => { createEffect(() => { config().relayUrls.map(async (relayUrl) => { - const relay = await pool().ensureRelay(relayUrl); - relay.on('notice', (msg: string) => { - console.error(`NOTICE: ${relayUrl}: ${msg}`); - }); + try { + const relay = await pool().ensureRelay(relayUrl); + relay.on('notice', (msg: string) => { + console.error(`NOTICE: ${relayUrl}: ${msg}`); + }); + } catch (err) { + console.error('ensureRelay failed', err); + } }); });