Fix Exception when grouping images in gallery

This commit is contained in:
Bojan Mojsilovic 2023-12-28 15:04:18 +01:00
parent a5a7359423
commit 9cf6bf5ffe
6 changed files with 527 additions and 332 deletions

View File

@ -44,7 +44,6 @@ const App: Component = () => {
<HomeProvider> <HomeProvider>
<ExploreProvider> <ExploreProvider>
<ThreadProvider> <ThreadProvider>
<input id="defocus" class={styles.invisible}/>
<Router /> <Router />
</ThreadProvider> </ThreadProvider>
</ExploreProvider> </ExploreProvider>

View File

@ -1,29 +1,18 @@
import { useIntl } from '@cookbook/solid-intl'; import { useIntl } from '@cookbook/solid-intl';
import { Component, createEffect, createSignal, For, Match, Show, Switch } from 'solid-js'; import { Component, createEffect, createSignal } 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 Modal from '../Modal/Modal'; import Modal from '../Modal/Modal';
import { useToastContext } from '../Toaster/Toaster'; 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 styles from './EnterPinModal.module.scss';
import { hookForDev } from '../../lib/devTools'; import { hookForDev } from '../../lib/devTools';
import ButtonPrimary from '../Buttons/ButtonPrimary'; import ButtonPrimary from '../Buttons/ButtonPrimary';
import ButtonLink from '../Buttons/ButtonLink';
import { useNavigate } from '@solidjs/router';
import TextInput from '../TextInput/TextInput'; import TextInput from '../TextInput/TextInput';
import ButtonSecondary from '../Buttons/ButtonSecondary'; import { decryptWithPin, setCurrentPin } from '../../lib/PrimalNostr';
import { decryptWithPin, encryptWithPin, setCurrentPin } from '../../lib/PrimalNostr';
const EnterPinModal: Component<{ const EnterPinModal: Component<{
id?: string, id?: string,
@ -43,9 +32,6 @@ const EnterPinModal: Component<{
const decWithPin = async () => { const decWithPin = async () => {
const val = props.valueToDecrypt || ''; const val = props.valueToDecrypt || '';
const dec = await decryptWithPin(pin(), val); const dec = await decryptWithPin(pin(), val);
// console.log('ENCODED: ', dec);
// console.log('PIN: ', pin());
// console.log('DECODE: ', decryptWithPin);
return dec; return dec;
}; };

View File

@ -27,10 +27,10 @@ const FeedSelect: Component<{ isPhone?: boolean, id?: string}> = (props) => {
const selectFeed = (option: FeedOption) => { const selectFeed = (option: FeedOption) => {
const [hex, includeReplies] = option.value?.split('_') || []; const [hex, includeReplies] = option.value?.split('_') || [];
const selector = document.getElementById('defocus'); // const selector = document.getElementById('defocus');
selector?.focus(); // selector?.focus();
selector?.blur(); // selector?.blur();
if (hex && !isSelected(option)) { if (hex && !isSelected(option)) {
const feed = findFeed(decodeURI(hex), includeReplies); const feed = findFeed(decodeURI(hex), includeReplies);

View File

@ -23,7 +23,7 @@ import {
import { truncateNpub, userName } from '../../stores/profile'; import { truncateNpub, userName } from '../../stores/profile';
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote'; import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
import { import {
Component, createEffect, createSignal, For, JSXElement, onMount, Show, Component, createSignal, For, JSXElement, onMount, Show,
} from 'solid-js'; } from 'solid-js';
import { import {
PrimalNote, PrimalNote,
@ -47,16 +47,6 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox';
const groupGridLimit = 7; 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) => { export const groupGalleryImages = (noteHolder: HTMLDivElement | undefined) => {
@ -147,6 +137,8 @@ const ParsedNote: Component<{
const intl = useIntl(); const intl = useIntl();
const media = useMediaContext(); const media = useMediaContext();
const dev = localStorage.getItem('devMode') === 'true';
const id = () => { const id = () => {
// if (props.id) return props.id; // if (props.id) return props.id;
@ -155,10 +147,6 @@ const ParsedNote: Component<{
let thisNote: HTMLDivElement | undefined; let thisNote: HTMLDivElement | undefined;
let imageGroup: string = generatePrivateKey()
let consecutiveImages: number = 0;
let imgCount = 0;
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
gallery: `#${id()}`, gallery: `#${id()}`,
children: `a.image_${props.note.post.noteId}`, children: `a.image_${props.note.post.noteId}`,
@ -173,29 +161,12 @@ const ParsedNote: Component<{
lightbox.init(); lightbox.init();
}); });
let allImagesLoaded = false;
createEffect(() => {
if (imagesLoaded() > 0 && imagesLoaded() === imgCount && !allImagesLoaded) {
allImagesLoaded = true;
groupGalleryImages(thisNote);
}
});
const [tokens, setTokens] = createStore<string[]>([]); const [tokens, setTokens] = createStore<string[]>([]);
const [imagesLoaded, setImagesLoaded] = createSignal(0);
let wordsDisplayed = 0; const [wordsDisplayed, setWordsDisplayed] = createSignal(0);
const shouldShowToken = () => { const isNoteTooLong = () => {
if (!props.shorten) return true; return props.shorten && wordsDisplayed() > shortNoteWords;
if (wordsDisplayed < shortNoteWords) {
return true;
}
return false;
}; };
const parseContent = () => { const parseContent = () => {
@ -208,20 +179,41 @@ const ParsedNote: Component<{
setTokens(() => [...tokens]); setTokens(() => [...tokens]);
} }
const parseToken: (token: string) => JSXElement = (token: string) => { type NoteContent = {
type: string,
tokens: string[],
meta?: Record<string, any>,
};
const [content, setContent] = createStore<NoteContent[]>([]);
const updateContent = (contentArray: NoteContent[], type: string, token: string, meta?: Record<string, any>) => {
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__') { if (token === '__LB__') {
return <br />; lastSignificantContent !== 'image' && updateContent(content, 'linebreak', token);
return;
} }
if (token === '__SP__') { if (token === '__SP__') {
return <> </>; lastSignificantContent !== 'image' && updateContent(content, 'text', ' ');
return;
} }
wordsDisplayed++;
if (isInterpunction(token)) { if (isInterpunction(token)) {
return <span>{token}</span>; lastSignificantContent = 'text';
updateContent(content, 'text', token);
return;
} }
if (isUrl(token)) { if (isUrl(token)) {
@ -234,26 +226,170 @@ const ParsedNote: Component<{
if (matched) { if (matched) {
const suffix = token.substring(matched.length + index, token.length); const suffix = token.substring(matched.length + index, token.length);
return <>{parseToken(prefix)}{parseToken(matched)}{parseToken(suffix)}</>;
parseToken(prefix);
parseToken(matched);
parseToken(suffix);
return;
} else { } else {
return <>{parseToken(prefix)}{parseToken(token.slice(index))}</>; parseToken(prefix);
parseToken(token.slice(index));
return;
} }
} }
if (!props.ignoreMedia) { if (!props.ignoreMedia) {
if (isImage(token)) { if (isImage(token)) {
imgCount++; lastSignificantContent = 'image';
consecutiveImages++; updateContent(content, 'image', token);
const dev = localStorage.getItem('devMode') === 'true'; 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;
}
}
if (props.noLinks === 'text') {
lastSignificantContent = 'text';
updateContent(content, 'text', token);
return;
}
lastSignificantContent = 'link';
updateContent(content, 'link', token);
return;
}
if (isNoteMention(token)) {
lastSignificantContent = 'notemention';
updateContent(content, 'notemention', token);
return;
}
if (isUserMention(token)) {
lastSignificantContent = 'usermention';
updateContent(content, 'usermention', token);
return;
}
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<tokens.length; i++) {
const token = tokens[i];
parseToken(token);
}
};
const renderLinebreak = (item: NoteContent) => {
if (isNoteTooLong()) return;
// Allow only one consecutive linebreak
return <br />
};
const renderText = (item: NoteContent) => {
return <For each={item.tokens}>
{token => {
if (isNoteTooLong()) return;
if (token.trim().length > 0) {
setWordsDisplayed(w => w + 1);
}
return token
}}
</For>;
};
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'); let image = media?.actions.getMedia(token, 'o');
const url = image?.media_url || getMediaUrlDefault(token); const url = image?.media_url || getMediaUrlDefault(token);
if (consecutiveImages > 1) { // Images tell a 100 words :)
// There are consecutive images, so reduce the impact of each image in order to show them grouped setWordsDisplayed(w => w + 100);
wordsDisplayed += 10;
} else {
wordsDisplayed += shortMentionInWords
}
return <NoteImage return <NoteImage
class={`noteimage image_${props.note.post.noteId}`} class={`noteimage image_${props.note.post.noteId}`}
@ -262,16 +398,43 @@ const ParsedNote: Component<{
media={image} media={image}
width={514} width={514}
imageGroup={imageGroup} imageGroup={imageGroup}
onImageLoaded={() => setImagesLoaded(i => i+1)}
shortHeight={props.shorten} shortHeight={props.shorten}
/>; />
} }
consecutiveImages = 0; const gridClass = groupCount < groupGridLimit ? `grid-${groupCount}` : 'grid-large';
imageGroup = generatePrivateKey();
return <div class={`imageGrid ${gridClass}`}>
<For each={item.tokens}>
{(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 <NoteImage
class={`noteimage_gallery image_${props.note.post.noteId} cell_${index()}`}
src={url}
isDev={dev}
media={image}
width={514}
imageGroup={imageGroup}
shortHeight={props.shorten}
plainBorder={true}
/>
}}
</For>
</div>
}
const renderVideo = (item: NoteContent) => {
return <For each={item.tokens}>{
(token) => {
if (isNoteTooLong()) return;
if (isMp4Video(token)) {
wordsDisplayed += shortMentionInWords;
let mVideo = media?.actions.getMedia(token, 'o'); let mVideo = media?.actions.getMedia(token, 'o');
let h: number | undefined = undefined; let h: number | undefined = undefined;
@ -284,74 +447,38 @@ const ParsedNote: Component<{
h = h > 680 ? 680 : h; h = h > 680 ? 680 : h;
} }
// const h = mVideo ? mVideo?.h > 524 ? 524 * mVideo?.h / mVideo?.w : mVideo?.h : undefined; let klass = mVideo ? 'w-cen' : 'w-max';
// const w = mVideo ? mVideo?.w > 524 ? 524 : mVideo?.w : undefined;
const klass = mVideo ? 'w-cen' : 'w-max';
const video = <video class={klass} width={w} height={h} controls muted={true} ><source src={token} type="video/mp4" /></video>; if (dev && !mVideo) {
media?.actions.addVideo(video as HTMLVideoElement); klass += ' redBorder';
return video;
} }
if (isOggVideo(token)) { setWordsDisplayed(w => w + shortMentionInWords);
wordsDisplayed += shortMentionInWords;
let mVideo = media?.actions.getMedia(token, 'o');
let h: number | undefined = undefined; const video = <video
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 =
<video
class={klass} class={klass}
width={w} width={w}
height={h} height={h}
controls controls
muted={true} muted={true}
> >
<source src={token} type="video/ogg" /> <source src={token} type={item.meta?.videoType} />
</video>; </video>;
media?.actions.addVideo(video as HTMLVideoElement); media?.actions.addVideo(video as HTMLVideoElement);
return video; return video;
} }
}</For>;
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 renderYouTube = (item: NoteContent) => {
// const w = mVideo ? mVideo?.w > 524 ? 524 : mVideo?.w : undefined;
const klass = mVideo ? 'w-cen' : 'w-max';
const video = <video class={klass} width={w} height={h} controls muted={true} ><source src={token} type="video/webm" /></video>; return <For each={item.tokens}>
media?.actions.addVideo(video as HTMLVideoElement); {(token) => {
return video; if (isNoteTooLong()) return;
}
if (isYouTube(token)) { setWordsDisplayed(w => w + shortMentionInWords);
wordsDisplayed += shortMentionInWords;
const youtubeId = isYouTube(token) && RegExp.$1; const youtubeId = isYouTube(token) && RegExp.$1;
@ -365,10 +492,16 @@ const ParsedNote: Component<{
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen allowFullScreen
></iframe>; ></iframe>;
} }}
</For>
};
if (isSpotify(token)) { const renderSpotify = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2"); const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
@ -382,10 +515,16 @@ const ParsedNote: Component<{
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy" loading="lazy"
></iframe>; ></iframe>;
} }}
</For>
};
if (isTwitch(token)) { const renderTwitch = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
const channel = token.split("/").slice(-1); const channel = token.split("/").slice(-1);
@ -397,10 +536,16 @@ const ParsedNote: Component<{
className="w-max" className="w-max"
allowFullScreen allowFullScreen
></iframe>; ></iframe>;
} }}
</For>
};
if (isMixCloud(token)) { const renderMixCloud = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2); const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2);
@ -414,10 +559,16 @@ const ParsedNote: Component<{
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=%2F${feedPath}%2F`} src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=%2F${feedPath}%2F`}
></iframe> ></iframe>
</div>; </div>;
} }}
</For>
};
if (isSoundCloud(token)) { const renderSoundCloud = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
return <iframe return <iframe
width="100%" width="100%"
@ -427,10 +578,16 @@ const ParsedNote: Component<{
allow="autoplay" allow="autoplay"
src={`https://w.soundcloud.com/player/?url=${token}`} src={`https://w.soundcloud.com/player/?url=${token}`}
></iframe>; ></iframe>;
} }}
</For>
};
if (isAppleMusic(token)) { const renderAppleMusic = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com"); const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com");
const isSongLink = /\?i=\d+$/.test(convertedUrl); const isSongLink = /\?i=\d+$/.test(convertedUrl);
@ -444,10 +601,16 @@ const ParsedNote: Component<{
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
src={convertedUrl} src={convertedUrl}
></iframe>; ></iframe>;
} }}
</For>
};
if (isWavelake(token)) { const renderWavelake = (item: NoteContent) => {
wordsDisplayed += shortMentionInWords; return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + shortMentionInWords);
const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com"); const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
@ -460,12 +623,14 @@ const ParsedNote: Component<{
frameBorder="0" frameBorder="0"
loading="lazy" loading="lazy"
></iframe>; ></iframe>;
} }}
} </For>
};
if (props.noLinks === 'text') { const renderLinks = (item: NoteContent) => {
return <span class="whole">{token}</span>; return <For each={item.tokens}>
} {(token) => {
if (isNoteTooLong()) return;
const preview = getLinkPreview(token); const preview = getLinkPreview(token);
@ -478,21 +643,25 @@ const ParsedNote: Component<{
); );
if (hasMinimalPreviewData) { if (hasMinimalPreviewData) {
wordsDisplayed += shortMentionInWords; setWordsDisplayed(w => w + shortMentionInWords);
return <LinkPreview preview={preview} bordered={props.isEmbeded} />; return <LinkPreview preview={preview} bordered={props.isEmbeded} />;
} }
setWordsDisplayed(w => w + 1);
return <span data-url={token}><a link href={token.toLowerCase()} target="_blank" >{token}</a></span>; return <span data-url={token}><a link href={token.toLowerCase()} target="_blank" >{token}</a></span>;
} }}
</For>
};
consecutiveImages = 0; const renderNoteMention = (item: NoteContent) => {
imageGroup = generatePrivateKey(); return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
if (isNoteMention(token)) {
let [_, id] = token.split(':'); let [_, id] = token.split(':');
if (!id) { if (!id) {
return token; return <>{token}</>;
} }
let end = ''; let end = '';
@ -524,7 +693,7 @@ const ParsedNote: Component<{
link = <A href={path}>{token}</A>; link = <A href={path}>{token}</A>;
if (ment) { if (ment) {
wordsDisplayed += shortMentionInWords; setWordsDisplayed(w => w + shortMentionInWords);
link = <div> link = <div>
<EmbeddedNote <EmbeddedNote
@ -536,17 +705,25 @@ const ParsedNote: Component<{
} }
} catch (e) { } catch (e) {
setWordsDisplayed(w => w + 1);
link = <span class={styles.error}>{token}</span>; link = <span class={styles.error}>{token}</span>;
} }
return <span class="whole"> {link}{end}</span>; return link;}}
} </For>
};
const renderUserMention = (item: NoteContent) => {
return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + 1);
if (isUserMention(token)) {
let [_, id] = token.split(':'); let [_, id] = token.split(':');
if (!id) { if (!id) {
return token; return <>{token}</>;
} }
let end = ''; let end = '';
@ -582,17 +759,23 @@ const ParsedNote: Component<{
<><A href={path}>@{label}</A>{end}</> : <><A href={path}>@{label}</A>{end}</> :
<>{MentionedUserLink({ user })}{end}</>; <>{MentionedUserLink({ user })}{end}</>;
} }
return link;
return <span class="whole"> {link}</span>;
} catch (e) { } catch (e) {
return <span class={styles.error}> {token}</span>; return <span class={styles.error}> {token}</span>;
} }
} }}
</For>
};
const renderTagMention = (item: NoteContent) => {
return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + 1);
if (isTagMention(token)) {
let t = `${token}`; let t = `${token}`;
let end = t[t.length - 1]; let end = t[t.length - 1];
if ([',', '?', ';', '!'].some(x => end === x)) { if ([',', '?', ';', '!'].some(x => end === x)) {
@ -628,7 +811,7 @@ const ParsedNote: Component<{
embeded = <><A href={path}>{noteId}</A>{end}</>; embeded = <><A href={path}>{noteId}</A>{end}</>;
if (ment) { if (ment) {
wordsDisplayed += shortMentionInWords; setWordsDisplayed(w => w + shortMentionInWords - 1);
embeded = <div> embeded = <div>
<EmbeddedNote <EmbeddedNote
@ -661,12 +844,19 @@ const ParsedNote: Component<{
<><A href={path}>@{label}</A>{end}</> : <><A href={path}>@{label}</A>{end}</> :
<>{MentionedUserLink({ user })}{end}</>; <>{MentionedUserLink({ user })}{end}</>;
} }
return <span> {link}</span>; return <span> {link}</span>;
} }
} }}
</For>
};
const renderHashtag = (item: NoteContent) => {
return <For each={item.tokens}>
{(token) => {
if (isNoteTooLong()) return;
setWordsDisplayed(w => w + 1);
if (isHashtag(token)) {
let [_, term] = token.split('#'); let [_, term] = token.split('#');
let end = ''; let end = '';
@ -683,25 +873,46 @@ const ParsedNote: Component<{
<A href={`/search/%23${term}`}>#{term}</A>; <A href={`/search/%23${term}`}>#{term}</A>;
return <span class="whole"> {embeded}{end}</span>; return <span class="whole"> {embeded}{end}</span>;
}}
</For>
};
const renderContent = (item: NoteContent) => {
const renderers: Record<string, (item: NoteContent) => 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 <span class="whole">{convertHTMLEntity(token)}</span>; return renderers[item.type] ?
renderers[item.type](item) :
<></>;
}; };
onMount(() => { onMount(() => {
parseContent(); generateContent();
}); });
return ( return (
<div ref={thisNote} id={id()} class={styles.parsedNote} > <div ref={thisNote} id={id()} class={styles.parsedNote} >
<For each={tokens}> <For each={content}>
{(token) => {(item) => renderContent(item)}
<Show when={shouldShowToken()}>
<>{parseToken(token)}</>
</Show>
}
</For> </For>
<Show when={props.shorten && tokens.length > shortNoteWords}> <Show when={isNoteTooLong()}>
<span class={styles.more}> <span class={styles.more}>
... <span class="linkish">{intl.formatMessage(actions.seeMore)}</span> ... <span class="linkish">{intl.formatMessage(actions.seeMore)}</span>
</span> </span>

View File

@ -2,7 +2,6 @@
import { Relay } from "nostr-tools"; import { Relay } from "nostr-tools";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import LinkPreview from "../components/LinkPreview/LinkPreview"; 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 { 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 { sendMessage, subscribeTo } from "../sockets";
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal"; import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";

View File

@ -125,7 +125,7 @@ const Thread: Component = () => {
scrollWindowTo(rect.top - header - banner); scrollWindowTo(rect.top - header - banner);
// repliesHolder.setAttribute('style', `height: ${document.documentElement.scrollHeight}px;`) // repliesHolder.setAttribute('style', `height: ${document.documentElement.scrollHeight}px;`)
}, 0) }, 1000)
} }
}); });