From e97eb9144c5bf46fd27bf4f7640adb7f2109364b Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Sat, 16 Dec 2023 10:45:57 +0900 Subject: [PATCH] feat: lazy load --- src/components/event/TextNote.tsx | 402 +++++++++--------- .../event/textNote/ImageDisplay.tsx | 27 +- .../event/textNote/MentionedEventDisplay.tsx | 17 +- .../event/textNote/PreviewedLink.tsx | 113 +++-- .../event/textNote/VideoDisplay.tsx | 27 +- src/components/modal/UserList.tsx | 10 +- src/components/utils/LazyLoad.tsx | 47 ++ 7 files changed, 377 insertions(+), 266 deletions(-) create mode 100644 src/components/utils/LazyLoad.tsx diff --git a/src/components/event/TextNote.tsx b/src/components/event/TextNote.tsx index fb09fcd..b27a873 100644 --- a/src/components/event/TextNote.tsx +++ b/src/components/event/TextNote.tsx @@ -32,6 +32,7 @@ import UserList from '@/components/modal/UserList'; import NotePostForm from '@/components/NotePostForm'; import Post from '@/components/Post'; import { useTimelineContext } from '@/components/timeline/TimelineContext'; +import LazyLoad from '@/components/utils/LazyLoad'; import useConfig from '@/core/useConfig'; import useModalState from '@/hooks/useModalState'; import { useTranslation } from '@/i18n/useTranslation'; @@ -44,6 +45,11 @@ import useReposts from '@/nostr/useReposts'; import ensureNonNull from '@/utils/ensureNonNull'; import timeout from '@/utils/timeout'; +export type ActionProps = { + event: NostrEvent; + onClickReply: () => void; +}; + export type TextNoteProps = { event: NostrEvent; embedding?: boolean; @@ -67,25 +73,19 @@ const emojiDataToReactionTypes = (emoji: EmojiData): ReactionTypes => { throw new Error('unknown emoji'); }; -const TextNote: Component = (props) => { +const Actions: Component = (props) => { const i18n = useTranslation(); const { config } = useConfig(); const pubkey = usePubkey(); - const { showProfile } = useModalState(); - const timelineContext = useTimelineContext(); + const commands = useCommands(); + const [modal, setModal] = createSignal<'EventDebugModal' | 'Reactions' | 'Reposts' | null>(null); const [reacted, setReacted] = createSignal(false); const [reposted, setReposted] = createSignal(false); - const [showReplyForm, setShowReplyForm] = createSignal(false); - const [modal, setModal] = createSignal<'EventDebugModal' | 'Reactions' | 'Reposts' | null>(null); - - const closeReplyForm = () => setShowReplyForm(false); - const closeModal = () => setModal(null); const event = createMemo(() => textNote(props.event)); - const embedding = () => props.embedding ?? true; - const actions = () => props.actions ?? true; + const closeModal = () => setModal(null); const { reactions, @@ -107,7 +107,18 @@ const TextNote: Component = (props) => { eventId: props.event.id, })); - const commands = useCommands(); + const isReactedByMe = createMemo(() => { + const p = pubkey(); + return (p != null && isReactedBy(p)) || reacted(); + }); + const isReactedByMeWithEmoji = createMemo(() => { + const p = pubkey(); + return p != null && isReactedByWithEmoji(p); + }); + const isRepostedByMe = createMemo(() => { + const p = pubkey(); + return (p != null && isRepostedBy(p)) || reposted(); + }); const publishReactionMutation = createMutation({ mutationKey: ['publishReaction', event().id], @@ -186,82 +197,6 @@ const TextNote: Component = (props) => { }, }); - const menu: MenuItem[] = [ - { - content: () => i18n()('post.copyEventId'), - onSelect: () => { - navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err)); - }, - }, - { - content: () => i18n()('post.showJSON'), - onSelect: () => { - setModal('EventDebugModal'); - }, - }, - { - content: () => i18n()('post.showReposts'), - onSelect: () => { - setModal('Reposts'); - }, - }, - { - content: () => i18n()('post.showReactions'), - onSelect: () => { - setModal('Reactions'); - }, - }, - { - when: () => event().pubkey === pubkey(), - content: () => {i18n()('post.deletePost')}, - onSelect: () => { - const p = pubkey(); - if (p == null) return; - - if (!window.confirm(i18n()('post.confirmDelete'))) return; - deleteMutation.mutate({ - relayUrls: config().relayUrls, - pubkey: p, - eventId: event().id, - }); - }, - }, - ]; - - const isReactedByMe = createMemo(() => { - const p = pubkey(); - return (p != null && isReactedBy(p)) || reacted(); - }); - const isReactedByMeWithEmoji = createMemo(() => { - const p = pubkey(); - return p != null && isReactedByWithEmoji(p); - }); - const isRepostedByMe = createMemo(() => { - const p = pubkey(); - return (p != null && isRepostedBy(p)) || reposted(); - }); - - const showReplyEvent = (): string | undefined => { - if (embedding()) { - const replyingToEvent = event().replyingToEvent(); - - if (replyingToEvent != null && !event().containsEventMention(replyingToEvent.id)) { - return replyingToEvent.id; - } - - const rootEvent = event().rootEvent(); - - if ( - replyingToEvent == null && - rootEvent != null && - !event().containsEventMention(rootEvent.id) - ) { - return rootEvent.id; - } - } - return undefined; - }; - const handleRepost: JSX.EventHandler = (ev) => { ev.stopPropagation(); @@ -308,6 +243,184 @@ const TextNote: Component = (props) => { doReaction(emojiDataToReactionTypes(emoji)); }; + const menu: MenuItem[] = [ + { + content: () => i18n()('post.copyEventId'), + onSelect: () => { + navigator.clipboard.writeText(noteEncode(props.event.id)).catch((err) => window.alert(err)); + }, + }, + { + content: () => i18n()('post.showJSON'), + onSelect: () => { + setModal('EventDebugModal'); + }, + }, + { + content: () => i18n()('post.showReposts'), + onSelect: () => { + setModal('Reposts'); + }, + }, + { + content: () => i18n()('post.showReactions'), + onSelect: () => { + setModal('Reactions'); + }, + }, + { + when: () => event().pubkey === pubkey(), + content: () => {i18n()('post.deletePost')}, + onSelect: () => { + const p = pubkey(); + if (p == null) return; + + if (!window.confirm(i18n()('post.confirmDelete'))) return; + deleteMutation.mutate({ + relayUrls: config().relayUrls, + pubkey: p, + eventId: event().id, + }); + }, + }, + ]; + + return ( + <> + 0}> + + +
+ +
+ + 0}> +
{reposts().length}
+
+
+
+ + 0}> +
{reactions().length}
+
+
+ +
+ + + + + +
+
+
+ + + + + +
+
+ + + + + + ev.pubkey} + renderInfo={(ev) => ( +
+ +
+ )} + onClose={closeModal} + /> +
+ + ev.pubkey} onClose={closeModal} /> + +
+ + ); +}; + +const TextNote: Component = (props) => { + const i18n = useTranslation(); + const { showProfile } = useModalState(); + const timelineContext = useTimelineContext(); + + const [showReplyForm, setShowReplyForm] = createSignal(false); + const closeReplyForm = () => setShowReplyForm(false); + const toggleReplyForm = () => setShowReplyForm((current) => !current); + + const event = createMemo(() => textNote(props.event)); + + const embedding = () => props.embedding ?? true; + const actions = () => props.actions ?? true; + + const showReplyEvent = (): string | undefined => { + if (embedding()) { + const replyingToEvent = event().replyingToEvent(); + + if (replyingToEvent != null && !event().containsEventMention(replyingToEvent.id)) { + return replyingToEvent.id; + } + + const rootEvent = event().rootEvent(); + + if ( + replyingToEvent == null && + rootEvent != null && + !event().containsEventMention(rootEvent.id) + ) { + return rootEvent.id; + } + } + return undefined; + }; + return (
= (props) => { {(id) => (
- + }> + {() => } +
)}
@@ -354,94 +469,9 @@ const TextNote: Component = (props) => { } actions={ - 0}> - - -
- -
- - 0}> -
{reposts().length}
-
-
-
- - 0 - } - > -
{reactions().length}
-
-
- -
- - - - - -
-
-
- - - - - -
-
+ }> + {() => } +
} footer={ @@ -461,26 +491,6 @@ const TextNote: Component = (props) => { timelineContext?.setTimeline({ type: 'Replies', event: props.event }); }} /> - - - - - - ev.pubkey} - renderInfo={(ev) => ( -
- -
- )} - onClose={closeModal} - /> -
- - ev.pubkey} onClose={closeModal} /> - -
); }; diff --git a/src/components/event/textNote/ImageDisplay.tsx b/src/components/event/textNote/ImageDisplay.tsx index 44257ac..2783c2c 100644 --- a/src/components/event/textNote/ImageDisplay.tsx +++ b/src/components/event/textNote/ImageDisplay.tsx @@ -1,5 +1,6 @@ import { Component, createSignal, Show } from 'solid-js'; +import LazyLoad from '@/components/utils/LazyLoad'; import SafeLink from '@/components/utils/SafeLink'; import { useTranslation } from '@/i18n/useTranslation'; import { thumbnailUrl } from '@/utils/url'; @@ -26,14 +27,24 @@ const ImageDisplay: Component = (props) => { } > - - {props.url} - + + + + } + > + {() => ( + + {props.url} + + )} + ); }; diff --git a/src/components/event/textNote/MentionedEventDisplay.tsx b/src/components/event/textNote/MentionedEventDisplay.tsx index 9a833db..10840cf 100644 --- a/src/components/event/textNote/MentionedEventDisplay.tsx +++ b/src/components/event/textNote/MentionedEventDisplay.tsx @@ -5,6 +5,7 @@ import { Kind } from 'nostr-tools'; // eslint-disable-next-line import/no-cycle import EventDisplayById from '@/components/event/EventDisplayById'; import EventLink from '@/components/EventLink'; +import LazyLoad from '@/components/utils/LazyLoad'; import { type MentionedEvent } from '@/nostr/parseTextNote'; export type MentionedEventDisplayProps = { @@ -17,12 +18,16 @@ const MentionedEventDisplay = (props: MentionedEventDisplayProps) => ( fallback={} >
- + + {() => ( + + )} +
); diff --git a/src/components/event/textNote/PreviewedLink.tsx b/src/components/event/textNote/PreviewedLink.tsx index 9b835dc..28c7575 100644 --- a/src/components/event/textNote/PreviewedLink.tsx +++ b/src/components/event/textNote/PreviewedLink.tsx @@ -1,5 +1,6 @@ -import { Component, JSX, Switch, Match, createEffect } from 'solid-js'; +import { Component, JSX, Switch, Match, createEffect, Show } from 'solid-js'; +import LazyLoad from '@/components/utils/LazyLoad'; import SafeLink from '@/components/utils/SafeLink'; import useConfig from '@/core/useConfig'; import { useOgp } from '@/utils/ogp'; @@ -28,65 +29,89 @@ const youtubeUrl = (videoId: string): string => { return iframeUrl.href; }; -const PreviewedLink: Component = (props) => { +const TwitterEmbed: Component<{ class?: string; href: string }> = (props) => { let twitterRef: HTMLQuoteElement | undefined; - const { config } = useConfig(); - - const { ogp } = useOgp(() => ({ - url: props.href, - })); - createEffect(() => { if (isTwitterUrl(props.href)) { window.twttr?.widgets?.load(twitterRef); } }); + return ( + + ); +}; + +const OgpEmbed: Component<{ url: string }> = (props) => { + const { ogp } = useOgp(() => ({ + url: props.url, + })); + + return ( + } keyed> + {(ogpProps) => ( + +
+ {ogpProps.title} +
+
{new URL(ogpProps.url).host}
+
{ogpProps.title}
+
{ogpProps.description}
+
+
+
+ )} +
+ ); +}; + +const PreviewedLink: Component = (props) => { + const { config } = useConfig(); + return ( }> - + {() => } {({ videoId }) => ( -
-