From 9cf6bf5ffe6d72882ac2f4b67de04a4e98d805da Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Thu, 28 Dec 2023 15:04:18 +0100 Subject: [PATCH] Fix Exception when grouping images in gallery --- src/App.tsx | 1 - .../EnterPinModal/EnterPinModal.tsx | 22 +- src/components/FeedSelect/FeedSelect.tsx | 6 +- src/components/ParsedNote/ParsedNote.tsx | 827 +++++++++++------- src/lib/notes.tsx | 1 - src/pages/Thread.tsx | 2 +- 6 files changed, 527 insertions(+), 332 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index db991ac..6f435f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,7 +44,6 @@ const App: Component = () => { - diff --git a/src/components/EnterPinModal/EnterPinModal.tsx b/src/components/EnterPinModal/EnterPinModal.tsx index df88410..22af902 100644 --- a/src/components/EnterPinModal/EnterPinModal.tsx +++ b/src/components/EnterPinModal/EnterPinModal.tsx @@ -1,29 +1,18 @@ import { useIntl } from '@cookbook/solid-intl'; -import { Component, createEffect, createSignal, For, Match, Show, Switch } from 'solid-js'; -import { useAccountContext } from '../../contexts/AccountContext'; -import { useSettingsContext } from '../../contexts/SettingsContext'; -import { zapNote } from '../../lib/zap'; -import { userName } from '../../stores/profile'; -import { toastZapFail, zapCustomOption } from '../../translations'; -import { PrimalNote } from '../../types/primal'; -import { debounce } from '../../utils'; +import { Component, createEffect, createSignal } from 'solid-js'; import Modal from '../Modal/Modal'; import { useToastContext } from '../Toaster/Toaster'; -import { base64 } from '@scure/base'; -import { nip19, utils } from 'nostr-tools'; +import { nip19 } from 'nostr-tools'; -import { login as tLogin, pin as tPin, actions as tActions } from '../../translations'; +import { pin as tPin, actions as tActions } from '../../translations'; import styles from './EnterPinModal.module.scss'; import { hookForDev } from '../../lib/devTools'; import ButtonPrimary from '../Buttons/ButtonPrimary'; -import ButtonLink from '../Buttons/ButtonLink'; -import { useNavigate } from '@solidjs/router'; import TextInput from '../TextInput/TextInput'; -import ButtonSecondary from '../Buttons/ButtonSecondary'; -import { decryptWithPin, encryptWithPin, setCurrentPin } from '../../lib/PrimalNostr'; +import { decryptWithPin, setCurrentPin } from '../../lib/PrimalNostr'; const EnterPinModal: Component<{ id?: string, @@ -43,9 +32,6 @@ const EnterPinModal: Component<{ const decWithPin = async () => { const val = props.valueToDecrypt || ''; const dec = await decryptWithPin(pin(), val); - // console.log('ENCODED: ', dec); - // console.log('PIN: ', pin()); - // console.log('DECODE: ', decryptWithPin); return dec; }; diff --git a/src/components/FeedSelect/FeedSelect.tsx b/src/components/FeedSelect/FeedSelect.tsx index e0cff4e..21dbfc6 100644 --- a/src/components/FeedSelect/FeedSelect.tsx +++ b/src/components/FeedSelect/FeedSelect.tsx @@ -27,10 +27,10 @@ const FeedSelect: Component<{ isPhone?: boolean, id?: string}> = (props) => { const selectFeed = (option: FeedOption) => { const [hex, includeReplies] = option.value?.split('_') || []; - const selector = document.getElementById('defocus'); + // const selector = document.getElementById('defocus'); - selector?.focus(); - selector?.blur(); + // selector?.focus(); + // selector?.blur(); if (hex && !isSelected(option)) { const feed = findFeed(decodeURI(hex), includeReplies); diff --git a/src/components/ParsedNote/ParsedNote.tsx b/src/components/ParsedNote/ParsedNote.tsx index 803b465..89c77a5 100644 --- a/src/components/ParsedNote/ParsedNote.tsx +++ b/src/components/ParsedNote/ParsedNote.tsx @@ -23,7 +23,7 @@ import { import { truncateNpub, userName } from '../../stores/profile'; import EmbeddedNote from '../EmbeddedNote/EmbeddedNote'; import { - Component, createEffect, createSignal, For, JSXElement, onMount, Show, + Component, createSignal, For, JSXElement, onMount, Show, } from 'solid-js'; import { PrimalNote, @@ -47,16 +47,6 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox'; const groupGridLimit = 7; -const convertHTMLEntity = (text: string) => { - const span = document.createElement('span'); - - return text - .replace(/&[#A-Za-z0-9]+;/gi, (entity)=> { - span.innerHTML = entity; - return span.innerText; - }); -} - export const groupGalleryImages = (noteHolder: HTMLDivElement | undefined) => { @@ -147,6 +137,8 @@ const ParsedNote: Component<{ const intl = useIntl(); const media = useMediaContext(); + const dev = localStorage.getItem('devMode') === 'true'; + const id = () => { // if (props.id) return props.id; @@ -155,10 +147,6 @@ const ParsedNote: Component<{ let thisNote: HTMLDivElement | undefined; - let imageGroup: string = generatePrivateKey() - let consecutiveImages: number = 0; - let imgCount = 0; - const lightbox = new PhotoSwipeLightbox({ gallery: `#${id()}`, children: `a.image_${props.note.post.noteId}`, @@ -173,29 +161,12 @@ const ParsedNote: Component<{ lightbox.init(); }); - let allImagesLoaded = false; - - createEffect(() => { - if (imagesLoaded() > 0 && imagesLoaded() === imgCount && !allImagesLoaded) { - allImagesLoaded = true; - groupGalleryImages(thisNote); - } - }); - const [tokens, setTokens] = createStore([]); - const [imagesLoaded, setImagesLoaded] = createSignal(0); - let wordsDisplayed = 0; + const [wordsDisplayed, setWordsDisplayed] = createSignal(0); - const shouldShowToken = () => { - if (!props.shorten) return true; - - - if (wordsDisplayed < shortNoteWords) { - return true; - } - - return false; + const isNoteTooLong = () => { + return props.shorten && wordsDisplayed() > shortNoteWords; }; const parseContent = () => { @@ -208,264 +179,458 @@ const ParsedNote: Component<{ setTokens(() => [...tokens]); } - const parseToken: (token: string) => JSXElement = (token: string) => { + type NoteContent = { + type: string, + tokens: string[], + meta?: Record, + }; - if (token === '__LB__') { - return
; + const [content, setContent] = createStore([]); + + const updateContent = (contentArray: NoteContent[], type: string, token: string, meta?: Record) => { + if (contentArray.length > 0 && contentArray[contentArray.length -1].type === type) { + setContent(content.length -1, 'tokens' , (els) => [...els, token]); + meta && setContent(content.length -1, 'meta' , () => ({ ...meta })); + return; + } + + setContent(content.length, () => ({ type, tokens: [token], meta })); + } + + let lastSignificantContent = 'text'; + + const parseToken = (token: string) => { + if (token === '__LB__') { + lastSignificantContent !== 'image' && updateContent(content, 'linebreak', token); + return; + } + + if (token === '__SP__') { + lastSignificantContent !== 'image' && updateContent(content, 'text', ' '); + return; + } + + if (isInterpunction(token)) { + lastSignificantContent = 'text'; + updateContent(content, 'text', token); + return; + } + + if (isUrl(token)) { + const index = token.indexOf('http'); + + if (index > 0) { + const prefix = token.slice(0, index); + + const matched = (token.match(urlExtractRegex) || [])[0]; + + if (matched) { + const suffix = token.substring(matched.length + index, token.length); + + parseToken(prefix); + parseToken(matched); + parseToken(suffix); + return; + } else { + parseToken(prefix); + parseToken(token.slice(index)); + return; + } } - if (token === '__SP__') { - return <> ; + if (!props.ignoreMedia) { + if (isImage(token)) { + lastSignificantContent = 'image'; + updateContent(content, 'image', token); + return; + } + + if (isMp4Video(token)) { + lastSignificantContent = 'video'; + updateContent(content, 'video', token, { videoType: 'video/mp4'}); + return; + } + + if (isOggVideo(token)) { + lastSignificantContent = 'video'; + updateContent(content, 'video', token, { videoType: 'video/ogg'}); + return; + } + + if (isWebmVideo(token)) { + lastSignificantContent = 'video'; + updateContent(content, 'video', token, { videoType: 'video/webm'}); + return; + } + + if (isYouTube(token)) { + lastSignificantContent = 'youtube'; + updateContent(content, 'youtube', token); + return; + } + + if (isSpotify(token)) { + lastSignificantContent = 'spotify'; + updateContent(content, 'spotify', token); + return; + } + + if (isTwitch(token)) { + lastSignificantContent = 'twitch'; + updateContent(content, 'twitch', token); + return; + } + + if (isMixCloud(token)) { + lastSignificantContent = 'mixcloud'; + updateContent(content, 'mixcloud', token); + return; + } + + if (isSoundCloud(token)) { + lastSignificantContent = 'soundcloud'; + updateContent(content, 'soundcloud', token); + return; + } + + if (isAppleMusic(token)) { + lastSignificantContent = 'applemusic'; + updateContent(content, 'applemusic', token); + return; + } + + if (isWavelake(token)) { + lastSignificantContent = 'wavelake'; + updateContent(content, 'wavelake', token); + return; + } } - wordsDisplayed++; - - if (isInterpunction(token)) { - return {token}; + if (props.noLinks === 'text') { + lastSignificantContent = 'text'; + updateContent(content, 'text', token); + return; } - if (isUrl(token)) { - const index = token.indexOf('http'); + lastSignificantContent = 'link'; + updateContent(content, 'link', token); + return; + } - if (index > 0) { - const prefix = token.slice(0, index); + if (isNoteMention(token)) { + lastSignificantContent = 'notemention'; + updateContent(content, 'notemention', token); + return; + } - const matched = (token.match(urlExtractRegex) || [])[0]; + if (isUserMention(token)) { + lastSignificantContent = 'usermention'; + updateContent(content, 'usermention', token); + return; + } - if (matched) { - const suffix = token.substring(matched.length + index, token.length); - return <>{parseToken(prefix)}{parseToken(matched)}{parseToken(suffix)}; - } else { - return <>{parseToken(prefix)}{parseToken(token.slice(index))}; - } + if (isTagMention(token)) { + lastSignificantContent = 'tagmention'; + updateContent(content, 'tagmention', token); + return; + } + + if (isHashtag(token)) { + lastSignificantContent = 'hashtag'; + updateContent(content, 'hashtag', token); + return; + } + + lastSignificantContent = 'text'; + updateContent(content, 'text', token); + return; + }; + + const generateContent = () => { + + parseContent(); + + for (let i=0; i { + if (isNoteTooLong()) return; + + // Allow only one consecutive linebreak + return
+ }; + + const renderText = (item: NoteContent) => { + return + {token => { + if (isNoteTooLong()) return; + if (token.trim().length > 0) { + setWordsDisplayed(w => w + 1); + } + return token + }} + ; + }; + + const renderImage = (item: NoteContent) => { + + const groupCount = item.tokens.length; + const imageGroup = generatePrivateKey(); + + if (groupCount === 1) { + if (isNoteTooLong()) return; + + const token = item.tokens[0]; + let image = media?.actions.getMedia(token, 'o'); + const url = image?.media_url || getMediaUrlDefault(token); + + // Images tell a 100 words :) + setWordsDisplayed(w => w + 100); + + return + } + + const gridClass = groupCount < groupGridLimit ? `grid-${groupCount}` : 'grid-large'; + + return
+ + {(token, index) => { + if (isNoteTooLong()) return; + + let image = media?.actions.getMedia(token, 'o'); + const url = image?.media_url || getMediaUrlDefault(token); + + // There are consecutive images, so reduce the impact of each image in order to show them grouped + setWordsDisplayed(w => w + 10 * groupCount); + + return + }} + +
+ } + + const renderVideo = (item: NoteContent) => { + return { + (token) => { + if (isNoteTooLong()) return; + + let mVideo = media?.actions.getMedia(token, 'o'); + + let h: number | undefined = undefined; + let w: number | undefined = undefined; + + if (mVideo) { + const ratio = mVideo.w / mVideo.h; + h = (524 / ratio); + w = h > 680 ? 680 * ratio : 524; + h = h > 680 ? 680 : h; } - if (!props.ignoreMedia) { - if (isImage(token)) { - imgCount++; - consecutiveImages++; - const dev = localStorage.getItem('devMode') === 'true'; - let image = media?.actions.getMedia(token, 'o'); - const url = image?.media_url || getMediaUrlDefault(token); + let klass = mVideo ? 'w-cen' : 'w-max'; - if (consecutiveImages > 1) { - // There are consecutive images, so reduce the impact of each image in order to show them grouped - wordsDisplayed += 10; - } else { - wordsDisplayed += shortMentionInWords - } - - return setImagesLoaded(i => i+1)} - shortHeight={props.shorten} - />; - } - - consecutiveImages = 0; - imageGroup = generatePrivateKey(); - - if (isMp4Video(token)) { - wordsDisplayed += shortMentionInWords; - let mVideo = media?.actions.getMedia(token, 'o'); - - let h: number | undefined = undefined; - let w: number | undefined = undefined; - - if (mVideo) { - const ratio = mVideo.w / mVideo.h; - h = (524 / ratio); - w = h > 680 ? 680 * ratio : 524; - h = h > 680 ? 680 : h; - } - - // const h = mVideo ? mVideo?.h > 524 ? 524 * mVideo?.h / mVideo?.w : mVideo?.h : undefined; - // const w = mVideo ? mVideo?.w > 524 ? 524 : mVideo?.w : undefined; - const klass = mVideo ? 'w-cen' : 'w-max'; - - const video = ; - media?.actions.addVideo(video as HTMLVideoElement); - - - return video; - } - - if (isOggVideo(token)) { - wordsDisplayed += shortMentionInWords; - let mVideo = media?.actions.getMedia(token, 'o'); - - let h: number | undefined = undefined; - let w: number | undefined = undefined; - - if (mVideo) { - const ratio = mVideo.w / mVideo.h; - h = (524 / ratio); - w = h > 680 ? 680 * ratio : 524; - h = h > 680 ? 680 : h; - } - - // const h = mVideo ? mVideo?.h > 524 ? 524 * mVideo?.h / mVideo?.w : mVideo?.h : undefined; - // const w = mVideo ? mVideo?.w > 524 ? 524 : mVideo?.w : undefined; - const klass = mVideo ? 'w-cen' : 'w-max'; - - const video = - ; - media?.actions.addVideo(video as HTMLVideoElement); - return video; - } - - if (isWebmVideo(token)) { - wordsDisplayed += shortMentionInWords; - let mVideo = media?.actions.getMedia(token, 'o'); - - let h: number | undefined = undefined; - let w: number | undefined = undefined; - - if (mVideo) { - const ratio = mVideo.w / mVideo.h; - h = (524 / ratio); - w = h > 680 ? 680 * ratio : 524; - h = h > 680 ? 680 : h; - } - - // const h = mVideo ? mVideo?.h > 524 ? 524 * mVideo?.h / mVideo?.w : mVideo?.h : undefined; - // const w = mVideo ? mVideo?.w > 524 ? 524 : mVideo?.w : undefined; - const klass = mVideo ? 'w-cen' : 'w-max'; - - const video = ; - media?.actions.addVideo(video as HTMLVideoElement); - return video; - } - - if (isYouTube(token)) { - wordsDisplayed += shortMentionInWords; - - const youtubeId = isYouTube(token) && RegExp.$1; - - return ; - } - - if (isSpotify(token)) { - wordsDisplayed += shortMentionInWords; - - const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2"); - - return ; - } - - if (isTwitch(token)) { - wordsDisplayed += shortMentionInWords; - - const channel = token.split("/").slice(-1); - - const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`; - - return ; - } - - if (isMixCloud(token)) { - wordsDisplayed += shortMentionInWords; - - const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2); - - return
- -
; - } - - if (isSoundCloud(token)) { - wordsDisplayed += shortMentionInWords; - - return ; - } - - if (isAppleMusic(token)) { - wordsDisplayed += shortMentionInWords; - - const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com"); - const isSongLink = /\?i=\d+$/.test(convertedUrl); - - return ; - } - - if (isWavelake(token)) { - wordsDisplayed += shortMentionInWords; - - const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com"); - - return ; - } + if (dev && !mVideo) { + klass += ' redBorder'; } - if (props.noLinks === 'text') { - return {token}; - } + setWordsDisplayed(w => w + shortMentionInWords); + + const video = ; + + media?.actions.addVideo(video as HTMLVideoElement); + + return video; + } + }
; + } + + const renderYouTube = (item: NoteContent) => { + + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const youtubeId = isYouTube(token) && RegExp.$1; + + return ; + }} + + }; + + const renderSpotify = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2"); + + return ; + }} + + }; + + const renderTwitch = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const channel = token.split("/").slice(-1); + + const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`; + + return ; + }} + + }; + + const renderMixCloud = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2); + + return
+ +
; + }} +
+ }; + + const renderSoundCloud = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + return ; + }} + + }; + + const renderAppleMusic = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com"); + const isSongLink = /\?i=\d+$/.test(convertedUrl); + + return ; + }} + + }; + + const renderWavelake = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + shortMentionInWords); + + const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com"); + + return ; + }} + + }; + + const renderLinks = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; const preview = getLinkPreview(token); @@ -478,21 +643,25 @@ const ParsedNote: Component<{ ); if (hasMinimalPreviewData) { - wordsDisplayed += shortMentionInWords; + setWordsDisplayed(w => w + shortMentionInWords); return ; } + setWordsDisplayed(w => w + 1); return {token}; - } + }} + + }; - consecutiveImages = 0; - imageGroup = generatePrivateKey(); + const renderNoteMention = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; - if (isNoteMention(token)) { let [_, id] = token.split(':'); if (!id) { - return token; + return <>{token}; } let end = ''; @@ -524,7 +693,7 @@ const ParsedNote: Component<{ link = {token}; if (ment) { - wordsDisplayed += shortMentionInWords; + setWordsDisplayed(w => w + shortMentionInWords); link =
w + 1); link = {token}; } - return {link}{end}; - } + return link;}} + + }; + + const renderUserMention = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + 1); - if (isUserMention(token)) { let [_, id] = token.split(':'); if (!id) { - return token; + return <>{token}; } let end = ''; @@ -582,17 +759,23 @@ const ParsedNote: Component<{ <>@{label}{end} : <>{MentionedUserLink({ user })}{end}; } - - return {link}; + return link; } catch (e) { return {token}; } - } + }} + + }; + + const renderTagMention = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + 1); - if (isTagMention(token)) { let t = `${token}`; - let end = t[t.length - 1]; if ([',', '?', ';', '!'].some(x => end === x)) { @@ -628,7 +811,7 @@ const ParsedNote: Component<{ embeded = <>{noteId}{end}; if (ment) { - wordsDisplayed += shortMentionInWords; + setWordsDisplayed(w => w + shortMentionInWords - 1); embeded =
@{label}{end} : <>{MentionedUserLink({ user })}{end}; } - return {link}; } - } + }} + + }; + + const renderHashtag = (item: NoteContent) => { + return + {(token) => { + if (isNoteTooLong()) return; + + setWordsDisplayed(w => w + 1); - if (isHashtag(token)) { let [_, term] = token.split('#'); let end = ''; @@ -683,25 +873,46 @@ const ParsedNote: Component<{ #{term}; return {embeded}{end}; - } + }} + + }; - return {convertHTMLEntity(token)}; + const renderContent = (item: NoteContent) => { + + const renderers: Record JSXElement> = { + linebreak: renderLinebreak, + text: renderText, + image: renderImage, + video: renderVideo, + youtube: renderYouTube, + spotify: renderSpotify, + twitch: renderTwitch, + mixcloud: renderMixCloud, + soundcloud: renderSoundCloud, + applemusic: renderAppleMusic, + wavelake: renderWavelake, + link: renderLinks, + notemention: renderNoteMention, + usermention: renderUserMention, + tagmention: renderTagMention, + hashtag: renderHashtag, + } + + return renderers[item.type] ? + renderers[item.type](item) : + <>; }; onMount(() => { - parseContent(); + generateContent(); }); return (
- - {(token) => - - <>{parseToken(token)} - - } + + {(item) => renderContent(item)} - shortNoteWords}> + ... {intl.formatMessage(actions.seeMore)} diff --git a/src/lib/notes.tsx b/src/lib/notes.tsx index ba817d8..fe1fe0e 100644 --- a/src/lib/notes.tsx +++ b/src/lib/notes.tsx @@ -2,7 +2,6 @@ import { Relay } from "nostr-tools"; import { createStore } from "solid-js/store"; import LinkPreview from "../components/LinkPreview/LinkPreview"; -import NoteImage from "../components/NoteImage/NoteImage"; import { appleMusicRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, mixCloudRegex, nostrNestsRegex, noteRegex, profileRegex, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants"; import { sendMessage, subscribeTo } from "../sockets"; import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal"; diff --git a/src/pages/Thread.tsx b/src/pages/Thread.tsx index c3b43f9..2326886 100644 --- a/src/pages/Thread.tsx +++ b/src/pages/Thread.tsx @@ -125,7 +125,7 @@ const Thread: Component = () => { scrollWindowTo(rect.top - header - banner); // repliesHolder.setAttribute('style', `height: ${document.documentElement.scrollHeight}px;`) - }, 0) + }, 1000) } });