diff --git a/packages/app/src/Components/Event/Create/NoteCreator.tsx b/packages/app/src/Components/Event/Create/NoteCreator.tsx index 0cbb61a9..6be676f8 100644 --- a/packages/app/src/Components/Event/Create/NoteCreator.tsx +++ b/packages/app/src/Components/Event/Create/NoteCreator.tsx @@ -30,6 +30,32 @@ import { ZapTarget } from "@/Utils/Zapper"; import FileUploadProgress from "../FileUpload"; import { OkResponseRow } from "./OkResponseRow"; +const previewNoteOptions = { + showContextMenu: false, + showFooter: false, + canClick: false, + showTime: false, +}; + +const replyToNoteOptions = { + showFooter: false, + showContextMenu: false, + showProfileCard: false, + showTime: false, + canClick: false, + showMedia: false, + longFormPreview: true, +}; + +const quoteNoteOptions = { + showFooter: false, + showContextMenu: false, + showTime: false, + canClick: false, + showMedia: false, + longFormPreview: true, +}; + export function NoteCreator() { const { formatMessage } = useIntl(); const uploader = useFileUpload(); @@ -293,17 +319,7 @@ export function NoteCreator() { function getPreviewNote() { if (note.preview) { - return ( - - ); + return ; } } @@ -600,18 +616,7 @@ export function NoteCreator() {

- + )} {note.quote && ( @@ -619,17 +624,7 @@ export function NoteCreator() {

- + )} {note.preview && getPreviewNote()} diff --git a/packages/app/src/Components/Event/Note/Note.tsx b/packages/app/src/Components/Event/Note/Note.tsx index b6e6ffb2..7696a4f7 100644 --- a/packages/app/src/Components/Event/Note/Note.tsx +++ b/packages/app/src/Components/Event/Note/Note.tsx @@ -1,12 +1,13 @@ -import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system"; +import { EventKind, NostrLink } from "@snort/system"; import classNames from "classnames"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { useInView } from "react-intersection-observer"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import NoteHeader from "@/Components/Event/Note/NoteHeader"; import { NoteText } from "@/Components/Event/Note/NoteText"; +import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo"; import useModeration from "@/Hooks/useModeration"; import { chainKey } from "@/Hooks/useThreadContext"; import { findTag } from "@/Utils"; @@ -19,128 +20,48 @@ import Poll from "../Poll"; import { NoteTranslation } from "./NoteContextMenu"; import NoteFooter from "./NoteFooter"; +const defaultOptions = { + showHeader: true, + showTime: true, + showFooter: true, + canUnpin: false, + canUnbookmark: false, + showContextMenu: true, +}; + +const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls]; + export function Note(props: NoteProps) { const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props; - - const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className); - const navigate = useNavigate(); - + const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? ""); const { isEventMuted } = useModeration(); const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" }); const [showTranslation, setShowTranslation] = useState(true); const [translated, setTranslated] = useState(); - const options = { - showHeader: true, - showTime: true, - showFooter: true, - canUnpin: false, - canUnbookmark: false, - showContextMenu: true, - ...opt, - }; + const optionsMerged = { ...defaultOptions, ...opt }; + const goToEvent = useGoToEvent(props, optionsMerged); - function goToEvent(e: React.MouseEvent, eTarget: TaggedNostrEvent) { - if (opt?.canClick === false) { - return; - } - - let target = e.target as HTMLElement | null; - while (target) { - if ( - target.tagName === "A" || - target.tagName === "BUTTON" || - target.classList.contains("reaction-pill") || - target.classList.contains("szh-menu-container") - ) { - return; // is there a better way to do this? - } - target = target.parentElement; - } - - e.stopPropagation(); - if (props.onClick) { - props.onClick(eTarget); - return; - } - - const link = NostrLink.fromEvent(eTarget); - // detect cmd key and open in new tab - if (e.metaKey) { - window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank"); - } else { - navigate(`/${link.encode(CONFIG.eventLinkPrefix)}`, { - state: eTarget, - }); - } - } - - const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls]; if (!canRenderAsTextNote.includes(ev.kind)) { - const alt = findTag(ev, "alt"); - if (alt) { - return ( -
- -
- ); - } else { - return ( - <> -

- -

-
{JSON.stringify(ev, undefined, "  ")}
- - ); - } - } - - function translation() { - if (translated && translated.confidence > 0.5) { - return ( - <> - { - e.stopPropagation(); - setShowTranslation(s => !s); - }}> - - - - ); - } else if (translated) { - return ( -

- -

- ); - } - } - - function pollOptions() { - if (ev.kind !== EventKind.Polls) return; - - return ; + return handleNonTextNote(ev); } function content() { - if (waitUntilInView && !inView) return undefined; + if (waitUntilInView && !inView) return null; return ( <> - {options.showHeader && } -
goToEvent(e, ev, true)}> + {optionsMerged.showHeader && } +
goToEvent(e, ev)}> - {translation()} - {pollOptions()} + {translated && } + {ev.kind === EventKind.Polls && }
- {options.showFooter && } + {optionsMerged.showFooter && } ); } - const note = ( + const noteElement = (
); - return !ignoreModeration && isEventMuted(ev) ? {note} : note; + return !ignoreModeration && isEventMuted(ev) ? {noteElement} : noteElement; +} + +function useGoToEvent(props, options) { + const navigate = useNavigate(); + return useCallback( + (e, eTarget) => { + if (options?.canClick === false) { + return; + } + + let target = e.target as HTMLElement | null; + while (target) { + if ( + target.tagName === "A" || + target.tagName === "BUTTON" || + target.classList.contains("reaction-pill") || + target.classList.contains("szh-menu-container") + ) { + return; + } + target = target.parentElement; + } + + e.stopPropagation(); + if (props.onClick) { + props.onClick(eTarget); + return; + } + + const link = NostrLink.fromEvent(eTarget); + if (e.metaKey) { + window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank"); + } else { + navigate(`/${link.encode(CONFIG.eventLinkPrefix)}`, { state: eTarget }); + } + }, + [navigate, props, options], + ); +} + +function handleNonTextNote(ev) { + const alt = findTag(ev, "alt"); + if (alt) { + return ( +
+ +
+ ); + } else { + return ( + <> +

+ +

+
{JSON.stringify(ev, undefined, "  ")}
+ + ); + } } diff --git a/packages/app/src/Components/Event/Note/NoteQuote.tsx b/packages/app/src/Components/Event/Note/NoteQuote.tsx index 7253a147..49f3838f 100644 --- a/packages/app/src/Components/Event/Note/NoteQuote.tsx +++ b/packages/app/src/Components/Event/Note/NoteQuote.tsx @@ -4,6 +4,11 @@ import { useEventFeed } from "@snort/system-react"; import Note from "@/Components/Event/EventComponent"; import PageSpinner from "@/Components/PageSpinner"; +const options = { + showFooter: false, + truncate: true, +}; + export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: number }) { const ev = useEventFeed(link); if (!ev.data) @@ -12,15 +17,5 @@ export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: nu
); - return ( - - ); + return ; } diff --git a/packages/app/src/Components/Event/Note/TranslationInfo.tsx b/packages/app/src/Components/Event/Note/TranslationInfo.tsx new file mode 100644 index 00000000..988efc01 --- /dev/null +++ b/packages/app/src/Components/Event/Note/TranslationInfo.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { NoteTranslation } from "@/Components/Event/Note/NoteContextMenu"; +import messages from "@/Components/messages"; + +interface TranslationInfoProps { + translated: NoteTranslation; + setShowTranslation: React.Dispatch>; +} + +export function TranslationInfo({ translated, setShowTranslation }: TranslationInfoProps) { + if (translated && translated.confidence > 0.5) { + return ( + <> + { + e.stopPropagation(); + setShowTranslation(show => !show); + }}> + + + + ); + } else if (translated) { + return ( +

+ +

+ ); + } + return null; +} diff --git a/packages/app/src/Components/Feed/Articles.tsx b/packages/app/src/Components/Feed/Articles.tsx index c7068e21..d93fff60 100644 --- a/packages/app/src/Components/Feed/Articles.tsx +++ b/packages/app/src/Components/Feed/Articles.tsx @@ -6,6 +6,10 @@ import { orderDescending } from "@/Utils"; import Note from "../Event/EventComponent"; +const options = { + longFormPreview: true, +}; + export default function Articles() { const data = useArticles(); const deck = useContext(DeckContext); @@ -16,9 +20,7 @@ export default function Articles() { { deck?.setArticle(ev); }} diff --git a/packages/app/src/Components/Trending/TrendingPosts.tsx b/packages/app/src/Components/Trending/TrendingPosts.tsx index 6c4db9b1..6c0756ad 100644 --- a/packages/app/src/Components/Trending/TrendingPosts.tsx +++ b/packages/app/src/Components/Trending/TrendingPosts.tsx @@ -1,7 +1,7 @@ import { removeUndefined } from "@snort/shared"; import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system"; import classNames from "classnames"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { ErrorOrOffline } from "@/Components/ErrorOrOffline"; import Note from "@/Components/Event/EventComponent"; @@ -41,6 +41,18 @@ export default function TrendingNotes({ count = Infinity, small = false }: { cou ); }); + const options = useMemo( + () => ({ + showFooter: !small, + showReactionsLink: !small, + showMedia: !small, + longFormPreview: !small, + truncate: small, + showContextMenu: !small, + }), + [small], + ); + const login = useLogin(); const displayAsInitial = small ? "list" : login.feedDisplayAs ?? "list"; const [displayAs, setDisplayAs] = useState(displayAsInitial); @@ -69,23 +81,11 @@ export default function TrendingNotes({ count = Infinity, small = false }: { cou }; const renderList = () => { - return filteredAndLimitedPosts.map(e => + return filteredAndLimitedPosts.map((e, index) => small ? ( ) : ( - + 5} /> ), ); }; diff --git a/packages/app/src/Components/User/Bookmarks.tsx b/packages/app/src/Components/User/Bookmarks.tsx index 42e02551..ede5da49 100644 --- a/packages/app/src/Components/User/Bookmarks.tsx +++ b/packages/app/src/Components/User/Bookmarks.tsx @@ -19,6 +19,10 @@ const Bookmarks = ({ pubkey, bookmarks }: BookmarksProps) => { const ps = useMemo(() => { return [...new Set(bookmarks.map(ev => ev.pubkey))]; }, [bookmarks]); + const options = useMemo( + () => ({ showTime: false, showBookmarked: true, canUnbookmark: publicKey === pubkey }), + [publicKey, pubkey], + ); function renderOption(p: HexKey) { const profile = UserCache.getFromCache(p); @@ -41,13 +45,7 @@ const Bookmarks = ({ pubkey, bookmarks }: BookmarksProps) => { {bookmarks .filter(b => (onlyPubkey === "all" ? true : b.pubkey === onlyPubkey)) .map(n => { - return ( - - ); + return ; })} ); diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 403bcc78..ea4ba119 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -1033,6 +1033,9 @@ "defaultMessage": "Redeem", "description": "Button: Redeem Cashu token" }, + "Y7FG5M": { + "defaultMessage": "Image not available" + }, "YDURw6": { "defaultMessage": "Service URL" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 11d85c7d..b5140d6c 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -340,6 +340,7 @@ "Xnimz0": "Sending from {wallet}", "Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.", "XrSk2j": "Redeem", + "Y7FG5M": "Image not available", "YDURw6": "Service URL", "YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.", "YXA3AH": "Enable reactions",