From c02d49e2f36cccc9d49fc8b611e4d36958ef5597 Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Wed, 18 Dec 2024 13:58:46 +0100 Subject: [PATCH] Blossom for images --- src/components/Avatar/Avatar.tsx | 1 + .../DirectMessages/DirectMessageContent.tsx | 2 + .../DirectMessageParsedContent.tsx | 3 + src/components/Note/NoteGallery.tsx | 1 + src/components/NoteImage/NoteImage.tsx | 31 +- src/components/PargingToken/ParsingToken.tsx | 559 ------------------ src/components/ParsedNote/ParsedNote.tsx | 2 + src/components/ProfileTabs/ProfileTabs.tsx | 10 +- src/constants.ts | 1 + src/contexts/AppContext.tsx | 14 +- src/pages/Longform.tsx | 1 + src/pages/ProfileDesktop.tsx | 2 + src/pages/ProfileMobile.tsx | 2 + src/types/primal.d.ts | 10 + 14 files changed, 70 insertions(+), 569 deletions(-) delete mode 100644 src/components/PargingToken/ParsingToken.tsx diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index ce36893c..fc9c8ad4 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -214,6 +214,7 @@ const Avatar: Component<{ onError={imgError} mediaThumb={imageThumb()} ignoreRatio={true} + authorPk={props.user?.pubkey} /> diff --git a/src/components/DirectMessages/DirectMessageContent.tsx b/src/components/DirectMessages/DirectMessageContent.tsx index 99c65326..28ac01c1 100644 --- a/src/components/DirectMessages/DirectMessageContent.tsx +++ b/src/components/DirectMessages/DirectMessageContent.tsx @@ -57,6 +57,7 @@ const DirectMessageConversation: Component<{ > ); @@ -105,6 +106,7 @@ const DirectMessageConversation: Component<{ > }> diff --git a/src/components/DirectMessages/DirectMessageParsedContent.tsx b/src/components/DirectMessages/DirectMessageParsedContent.tsx index c60f2ff3..644f357f 100644 --- a/src/components/DirectMessages/DirectMessageParsedContent.tsx +++ b/src/components/DirectMessages/DirectMessageParsedContent.tsx @@ -35,6 +35,7 @@ const groupGridLimit = 7; const DirectMessageParsedContent: Component<{ id?: string, content: string, + sender: string, ignoreMedia?: boolean, noLinks?: string, noPreviews?: boolean, @@ -412,6 +413,7 @@ const DirectMessageParsedContent: Component<{ mediaThumb={imageThumb} width={514} imageGroup={`${imageGroup}`} + authorPk={props.sender} /> } @@ -439,6 +441,7 @@ const DirectMessageParsedContent: Component<{ imageGroup={`${imageGroup}`} plainBorder={true} forceHeight={500} + authorPk={props.sender} /> }} diff --git a/src/components/Note/NoteGallery.tsx b/src/components/Note/NoteGallery.tsx index 3a4359d4..949fe16a 100644 --- a/src/components/Note/NoteGallery.tsx +++ b/src/components/Note/NoteGallery.tsx @@ -214,6 +214,7 @@ const NoteGallery: Component<{ } + authorPk={props.note.pubkey} /> diff --git a/src/components/NoteImage/NoteImage.tsx b/src/components/NoteImage/NoteImage.tsx index 7f6a508f..38740658 100644 --- a/src/components/NoteImage/NoteImage.tsx +++ b/src/components/NoteImage/NoteImage.tsx @@ -2,6 +2,7 @@ import { Component, createEffect, createSignal, JSX, JSXElement, onMount, Show import styles from "./NoteImage.module.scss"; import { generatePrivateKey } from "../../lib/nTools"; import { MediaVariant } from "../../types/primal"; +import { useAppContext } from "../../contexts/AppContext"; const NoteImage: Component<{ class?: string, @@ -19,7 +20,9 @@ const NoteImage: Component<{ caption?: JSXElement | string, ignoreRatio?: boolean, forceHeight?: number; + authorPk?: string, }> = (props) => { + const app = useAppContext(); const imgId = generatePrivateKey(); let imgVirtual: HTMLImageElement | undefined; @@ -31,7 +34,7 @@ const NoteImage: Component<{ const isCached = () => !props.isDev || props.media; - const onError = (event: any) => { + const onError = async (event: any) => { const image = event.target; if (image.src === props.altSrc || !props.altSrc || image.src.endsWith(props.altSrc)) { @@ -40,6 +43,32 @@ const NoteImage: Component<{ return true; } + const userBlossoms = app?.actions.getUserBlossomUrls(props.authorPk || ''); + + if (userBlossoms) { + const reqs = userBlossoms.map(url => new Promise((res, rej) => { + fetch(url, { method: 'HEAD'}).then((response) => { + if (response.headers.get('Content-Type')?.startsWith('image')) { + res(url); + } else { + rej('') + } + }); + })); + + const bServer = await Promise.any(reqs); + + if (typeof bServer === 'string' && bServer.length > 0) { + const oSrc = src() || ''; + const bSrc = oSrc.slice(oSrc.lastIndexOf('/'), oSrc.lastIndexOf('.')); + setSrc(() => `${bServer}/${bSrc}`); + + image.onerror = ""; + image.src = src(); + return true; + } + } + setSrc(() => props.altSrc || ''); image.onerror = ""; diff --git a/src/components/PargingToken/ParsingToken.tsx b/src/components/PargingToken/ParsingToken.tsx deleted file mode 100644 index 9ba4df35..00000000 --- a/src/components/PargingToken/ParsingToken.tsx +++ /dev/null @@ -1,559 +0,0 @@ -import { A } from '@solidjs/router'; -import { - addLinkPreviews, - isAppleMusic, - isImage, - isInterpunction, - isLinebreak, - isMixCloud, - isMp4Video, - isNoteMention, - isOggVideo, - isSoundCloud, - isSpotify, - isTwitch, - isUrl, - isUserMention, - isWavelake, - isWebmVideo, - isYouTube, -} from '../../lib/notes'; -import { convertToUser, truncateNpub, userName } from '../../stores/profile'; -import EmbeddedNote from '../EmbeddedNote/EmbeddedNote'; -import { - Component, createEffect, JSXElement, Match, Show, Switch, -} from 'solid-js'; -import { - NostrMentionContent, - NostrNoteContent, - NostrPostStats, - NostrStatsContent, - NostrUserContent, - NoteReference, - PrimalNote, - PrimalUser, - UserReference, -} from '../../types/primal'; - -import { nip19 } from '../../lib/nTools'; -import LinkPreview from '../LinkPreview/LinkPreview'; -import MentionedUserLink from '../Note/MentionedUserLink/MentionedUserLink'; -import { getMediaUrl, getMediaUrl as getMediaUrlDefault } from "../../lib/media"; -import NoteImage from '../NoteImage/NoteImage'; -import { createStore } from 'solid-js/store'; -import { Kind } from '../../constants'; -import { APP_ID } from '../../App'; -import { getEvents } from '../../lib/feed'; -import { getUserProfileInfo } from '../../lib/profile'; -import { subsTo } from '../../sockets'; -import { convertToNotes } from '../../stores/note'; -import { useAccountContext } from '../../contexts/AccountContext'; -import { logError } from '../../lib/logger'; -import { useAppContext } from '../../contexts/AppContext'; - - -export type Token = { - type: string; - content: string | PrimalNote | PrimalUser, - options?: Object, -} - -export type ParserContextStore = { - userRefs: UserReference, - noteRefs: NoteReference, - parsedToken: Token, - isDataFetched: boolean, - renderedUrl: JSXElement, -} - - -const ParsingToken: Component<{ - token: string, - userRefs?: UserReference, - noteRefs?: NoteReference, - id?: string, - ignoreMedia?: boolean, - noLinks?: 'links' | 'text', - noPreviews?: boolean, - index?: number, -}> = (props) => { - - const account = useAccountContext(); - const app = useAppContext(); - - const [store, updateStore] = createStore({ - userRefs: {}, - noteRefs: {}, - parsedToken: { type: 'text', content: ''}, - isDataFetched: false, - renderedUrl: <>, - }); - - const getMentionedUser = (mention: string) => { - let [_, npub] = mention.trim().split(':'); - - const lastChar = npub[npub.length - 1]; - - if (isInterpunction(lastChar)) { - npub = npub.slice(0, -1); - } - - const subId = `um_${APP_ID}`; - - try { - const eventId = nip19.decode(npub).data as string | nip19.ProfilePointer; - const hex = typeof eventId === 'string' ? eventId : eventId.pubkey; - - if (store.userRefs[hex]) { - return; - } - - const unsub = subsTo(subId, { - onEvent: (_, content) => { - if (!content) return; - - if (content.kind === Kind.Metadata) { - const user = content as NostrUserContent; - - const u = convertToUser(user, content.pubkey); - - updateStore('userRefs', () => ({ [u.pubkey]: u })); - return; - } - }, - onEose: () => { - updateStore('isDataFetched', () => true) - unsub(); - }, - }); - - getUserProfileInfo(hex, account?.publicKey, subId); - } - catch (e) { - logError('Failed to fetch mentioned user info: ', e); - } - } - - const getMentionedNote = (mention: string) => { - let [_, noteId] = mention.trim().split(':'); - - const lastChar = noteId[noteId.length - 1]; - - if (isInterpunction(lastChar)) { - noteId = noteId.slice(0, -1); - } - - const subId = `nm_${noteId}_${APP_ID}`; - - try{ - const eventId = nip19.decode(noteId).data as string | nip19.EventPointer; - const hex = typeof eventId === 'string' ? eventId : eventId.id; - - if (store.noteRefs[hex]) { - return; - } - - let users: Record = {}; - let messages: NostrNoteContent[] = []; - let noteStats: NostrPostStats = {}; - let noteMentions: Record = {}; - - const unsub = subsTo(subId, { - onEvent: (_, content) => { - - if (!content) return; - - if (content.kind === Kind.Metadata) { - const user = content as NostrUserContent; - - users[user.pubkey] = { ...user }; - return; - } - - if ([Kind.Text, Kind.Repost].includes(content.kind)) { - const message = content as NostrNoteContent; - - messages.push(message); - return; - } - - if (content.kind === Kind.NoteStats) { - const statistic = content as NostrStatsContent; - const stat = JSON.parse(statistic.content); - - noteStats[stat.event_id] = { ...stat }; - return; - } - - if (content.kind === Kind.Mentions) { - const mentionContent = content as NostrMentionContent; - const mention = JSON.parse(mentionContent.content); - - noteMentions[mention.id] = { ...mention }; - return; - } - }, - onEose: () => { - // @ts-ignore - const newNote = convertToNotes({ - users, - messages, - postStats: noteStats, - mentions: noteMentions, - noteActions: {}, - })[0]; - - updateStore('noteRefs', () => ({[newNote.post.id]: { ...newNote }})); - updateStore('isDataFetched', () => true) - unsub(); - }, - }); - - - getEvents(account?.publicKey, [hex], subId, true); - } - catch (e) { - logError('Failed to fetch mentioned user info: ', e); - } - } - - const prepareForParsing = async (token: string) => { - if (isUserMention(token)) { - getMentionedUser(token); - return; - } - - if (isNoteMention(token)) { - getMentionedNote(token); - return; - } - } - - - createEffect(() => { - prepareForParsing(props.token); - }); - - createEffect(() => { - updateStore('userRefs', props.userRefs || {}); - updateStore('noteRefs', props.noteRefs || {}); - }); - - createEffect(() => { - if (!isUrl(props.token)) return; - - if (props.noLinks === 'text') { - updateStore('renderedUrl', () => renderText(props.token)); - return; - } - - const url = props.token.trim(); - - updateStore('renderedUrl', () => {url}); - - addLinkPreviews(url).then(preview => { - const hasMinimalPreviewData = !props.noPreviews && - preview && - preview.url && - ((preview.description && preview.description.length > 0) || - preview.image || - preview.title - ); - - if (hasMinimalPreviewData) { - updateStore('renderedUrl', () =>
); - } - }); - }); - - const renderText = (token: string) => token; - - const renderImage = (token: string) => { - - const dev = localStorage.getItem('devMode') === 'true'; - let imgUrl = getMediaUrl ? getMediaUrl(token) : token; - const url = imgUrl || getMediaUrlDefault(token) - - return ; - } - - const renderVideo = (token: string, type: string) => { - return - } - - const renderYouTube = (token: string) => { - const youtubeId = isYouTube(token) && RegExp.$1; - - return ( - - ); - } - - const renderSpotify = (token: string) => { - const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2"); - - return ( - - ); - }; - - const renderTwitch = (token: string) => { - const channel = token.split("/").slice(-1); - const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`; - - return ( - - ); - }; - - const renderMixCloud = (token: string) => { - const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2); - - return ( - - ); - }; - - const renderSoundCloud = (token: string) => { - return ( - - ); - }; - - const renderAppleMusic = (token: string) => { - const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com"); - const isSongLink = /\?i=\d+$/.test(convertedUrl); - - return ( - - ); - }; - - const renderWavelake = (token: string) => { - const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com"); - - return ( - - ); - }; - - const renderNoteMention = (token: string) => { - let [nostr, noteId] = token.trim().split(':'); - - if (!noteId) { - return renderText(token); - } - - let lastChar = noteId[noteId.length - 1]; - - if (isInterpunction(lastChar)) { - noteId = noteId.slice(0, -1); - } else { - lastChar = ''; - } - - try { - const eventId = nip19.decode(noteId).data as string | nip19.EventPointer; - const hex = typeof eventId === 'string' ? eventId : eventId.id; - - const path = `/e/${noteId}`; - - if (props.noLinks === 'links') { - return <>{nostr}:{noteId}{lastChar}; - } - - if (!props.noLinks) { - const ment = store.noteRefs && store.noteRefs[hex]; - - return ment ? - <> - - {lastChar} - : - <>{nostr}:{noteId}{lastChar}; - } - - } catch (e) { - logError('Failed to render note mention: ', e) - return {token}; - } - } - - const renderUserMention = (token: string) => { - - let [_, npub] = token.trim().split(':'); - - if (!npub) { - return renderText(token); - } - - let lastChar = npub[npub.length - 1]; - - if (isInterpunction(lastChar)) { - npub = npub.slice(0, -1); - } else { - lastChar = ''; - } - - try { - const profileId = nip19.decode(npub).data as string | nip19.ProfilePointer; - - const hex = typeof profileId === 'string' ? profileId : profileId.pubkey; - - const path = app?.actions.profileLink(npub) || ''; - - let user = store.userRefs && store.userRefs[hex]; - - const label = user ? userName(user) : truncateNpub(npub); - - if (props.noLinks === 'links') { - return <>@{label}{lastChar}; - } - - if (!props.noLinks) { - return !user ? <>@{label}{lastChar} : <>{MentionedUserLink({ user })}{lastChar}; - } - - } catch (e) { - logError('Failed to parse user mention: ', e) - return {token}; - } - } - - return ( - - '}> - <> - - - - - - - {renderImage(props.token)} - - - - {renderVideo(props.token, 'mp4')} - - - - {renderVideo(props.token, 'ogg')} - - - - {renderVideo(props.token, 'webm')} - - - - {renderYouTube(props.token)} - - - - {renderSpotify(props.token)} - - - - {renderTwitch(props.token)} - - - - {renderMixCloud(props.token)} - - - - {renderSoundCloud(props.token)} - - - - {renderAppleMusic(props.token)} - - - - {renderWavelake(props.token)} - - - - - -
-
- - - {renderText(props.token)} - - - - - {renderNoteMention(props.token)} - - - - - - {renderUserMention(props.token)} - - - -
- ); -}; - -export default ParsingToken; diff --git a/src/components/ParsedNote/ParsedNote.tsx b/src/components/ParsedNote/ParsedNote.tsx index e7dc1c60..3a1568b8 100644 --- a/src/components/ParsedNote/ParsedNote.tsx +++ b/src/components/ParsedNote/ParsedNote.tsx @@ -608,6 +608,7 @@ const ParsedNote: Component<{ imageGroup={`${imageGroup}`} shortHeight={props.shorten} onError={imageError} + authorPk={props.note.pubkey} /> } @@ -648,6 +649,7 @@ const ParsedNote: Component<{ plainBorder={true} forceHeight={500} onError={imageError} + authorPk={props.note.pubkey} /> }} diff --git a/src/components/ProfileTabs/ProfileTabs.tsx b/src/components/ProfileTabs/ProfileTabs.tsx index 7751f266..79a5c345 100644 --- a/src/components/ProfileTabs/ProfileTabs.tsx +++ b/src/components/ProfileTabs/ProfileTabs.tsx @@ -1,28 +1,22 @@ import { useIntl } from "@cookbook/solid-intl"; import { Tabs } from "@kobalte/core/tabs"; import { A, useLocation } from "@solidjs/router"; -import PhotoSwipeLightbox from "photoswipe/lightbox"; import { Component, createEffect, createSignal, For, Match, on, onCleanup, onMount, Show, Switch } from "solid-js"; -import { createStore, unwrap } from "solid-js/store"; -import { imageOrVideoRegex, imageOrVideoRegexG, imageRegex, imageRegexG, Kind, profileContactListPage } from "../../constants"; +import { createStore } from "solid-js/store"; +import { imageOrVideoRegex, Kind, profileContactListPage } from "../../constants"; import { useAccountContext } from "../../contexts/AccountContext"; import { useMediaContext } from "../../contexts/MediaContext"; import { useProfileContext } from "../../contexts/ProfileContext"; -import { date } from "../../lib/dates"; import { hookForDev } from "../../lib/devTools"; import { humanizeNumber } from "../../lib/stats"; -import { store } from "../../services/StoreService"; import { userName } from "../../stores/profile"; import { profile as t, actions as tActions } from "../../translations"; import { PrimalNote, PrimalUser, PrimalZap } from "../../types/primal"; import ArticlePreview from "../ArticlePreview/ArticlePreview"; -import Avatar from "../Avatar/Avatar"; import ButtonCopy from "../Buttons/ButtonCopy"; import Loader from "../Loader/Loader"; import Note from "../Note/Note"; -import NoteImage from "../NoteImage/NoteImage"; import Paginator from "../Paginator/Paginator"; -import ProfileContact from "../ProfileContact/ProfileContact"; import styles from "./ProfileTabs.module.scss"; import NoteGallery from "../Note/NoteGallery"; diff --git a/src/constants.ts b/src/constants.ts index 5e0b1075..53914af6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -110,6 +110,7 @@ export enum Kind { MuteList = 10_000, RelayList = 10_002, Bookmarks = 10_003, + Blossom = 10_063, TierList = 17_000, CategorizedPeople = 30_000, diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 96840692..82e214c0 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -7,7 +7,7 @@ import { onMount, useContext } from "solid-js"; -import { MediaEvent, MediaVariant, NostrEOSE, NostrEvent, NostrEventContent, NostrEvents, PrimalArticle, PrimalDVM, PrimalNote, PrimalUser, ZapOption } from "../types/primal"; +import { MediaEvent, MediaVariant, NostrBlossom, NostrEOSE, NostrEvent, NostrEventContent, NostrEvents, PrimalArticle, PrimalDVM, PrimalNote, PrimalUser, ZapOption } from "../types/primal"; import { CashuMint } from "@cashu/cashu-ts"; import { Tier, TierCost } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal"; import { connect, disconnect, isConnected, isNotConnected, readData, refreshSocketListeners, removeSocketListeners, socket } from "../sockets"; @@ -109,6 +109,7 @@ export type AppContextStore = { removeConnectedRelay: (relay: Relay) => void, profileLink: (pubkey: string | undefined) => string, setLegendCustomization: (pubkey: string, config: LegendCustomizationConfig) => void, + getUserBlossomUrls: (pubkey: string) => string[], }, } @@ -305,6 +306,16 @@ export const AppProvider = (props: { children: JSXElement }) => { updateStore('legendCustomization', () => ({ [pubkey]: { ...config }})); } + const getUserBlossomUrls = (pubkey: string) => { + const blossom = store.events[Kind.Blossom].find(b => b.pubkey === pubkey) as NostrBlossom | undefined; + + if (!blossom || !blossom.tags) return []; + + return blossom.tags.reduce((acc, t) => { + return t[0] === 'server' ? [ ...acc, t[1]] : acc; + }, []); + } + // SOCKET HANDLERS ------------------------------ @@ -467,6 +478,7 @@ const onSocketClose = (closeEvent: CloseEvent) => { removeConnectedRelay, profileLink, setLegendCustomization, + getUserBlossomUrls, } }); diff --git a/src/pages/Longform.tsx b/src/pages/Longform.tsx index c338bad7..8ce51153 100644 --- a/src/pages/Longform.tsx +++ b/src/pages/Longform.tsx @@ -1040,6 +1040,7 @@ const Longform: Component< { naddr: string } > = (props) => { media={articleMediaImage()} mediaThumb={articleMediaThumb()} width={640} + authorPk={store.article?.pubkey} /> diff --git a/src/pages/ProfileDesktop.tsx b/src/pages/ProfileDesktop.tsx index 0a86c151..b0433c19 100644 --- a/src/pages/ProfileDesktop.tsx +++ b/src/pages/ProfileDesktop.tsx @@ -691,6 +691,7 @@ const ProfileDesktop: Component = () => { width={640} media={media?.actions.getMedia(banner() || '', 'o')} mediaThumb={media?.actions.getMedia(banner() || '', 'm') || media?.actions.getMedia(banner() || '', 'o') || banner()} + authorPk={profile?.profileKey} /> @@ -733,6 +734,7 @@ const ProfileDesktop: Component = () => { media={media?.actions.getMedia(banner() || '', 'o')} mediaThumb={media?.actions.getMedia(banner() || '', 'm') || media?.actions.getMedia(banner() || '', 'o') || banner()} ignoreRatio={true} + authorPk={profile?.profileKey} /> diff --git a/src/pages/ProfileMobile.tsx b/src/pages/ProfileMobile.tsx index 442975e1..29eb94a4 100644 --- a/src/pages/ProfileMobile.tsx +++ b/src/pages/ProfileMobile.tsx @@ -683,6 +683,7 @@ const ProfileMobile: Component = () => { width={640} media={media?.actions.getMedia(banner() || '', 'o')} mediaThumb={media?.actions.getMedia(banner() || '', 'm') || media?.actions.getMedia(banner() || '', 'o') || banner()} + authorPk={profile?.profileKey} /> @@ -707,6 +708,7 @@ const ProfileMobile: Component = () => { media={media?.actions.getMedia(banner() || '', 'o')} mediaThumb={media?.actions.getMedia(banner() || '', 'm') || media?.actions.getMedia(banner() || '', 'o') || banner()} ignoreRatio={true} + authorPk={profile?.profileKey} /> diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts index 25b186e3..9652c663 100644 --- a/src/types/primal.d.ts +++ b/src/types/primal.d.ts @@ -440,6 +440,15 @@ export type NostrMembershipCohortInfo= { id?: string, }; +export type NostrBlossom= { + kind: Kind.Blossom, + content: string, + created_at?: number, + pubkey?: string, + id?: string, + tags?: string[], +}; + export type NostrEventContent = NostrNoteContent | NostrUserContent | @@ -491,6 +500,7 @@ export type NostrEventContent = NostrLegendCustomization | NostrBroadcastStatus | NostrMembershipCohortInfo | + NostrBlossom | NostrTopicStats; export type NostrEvent = [