mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
New tokenized note rendering
This commit is contained in:
parent
b6f3869c58
commit
b36e3b9be9
@ -5,13 +5,12 @@ import { Component, createMemo, JSXElement, Show } from 'solid-js';
|
||||
import { useMediaContext } from '../../contexts/MediaContext';
|
||||
import { useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { date } from '../../lib/dates';
|
||||
import { parseNote2 } from '../../lib/notes';
|
||||
import { trimVerification } from '../../lib/profile';
|
||||
import { nip05Verification, userName } from '../../stores/profile';
|
||||
import { note as t } from '../../translations';
|
||||
import { PrimalNote, PrimalUser } from '../../types/primal';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import { parseNoteLinks, parseNpubLinks } from '../ParsedNote/ParsedNote';
|
||||
import ParsedNote from '../ParsedNote/ParsedNote';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
|
||||
import styles from './EmbeddedNote.module.scss';
|
||||
@ -20,7 +19,6 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
const intl = useIntl();
|
||||
const media = useMediaContext();
|
||||
|
||||
const noteId = () => nip19.noteEncode(props.note.post.id);
|
||||
|
||||
@ -32,77 +30,6 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string
|
||||
return trimVerification(props.note.user?.nip05);
|
||||
});
|
||||
|
||||
const parsedContent = (text: string) => {
|
||||
const regex = /\#\[([0-9]*)\]/g;
|
||||
let parsed = text;
|
||||
|
||||
let refs = [];
|
||||
let match;
|
||||
|
||||
while((match = regex.exec(text)) !== null) {
|
||||
refs.push(match[1]);
|
||||
}
|
||||
|
||||
if (refs.length > 0) {
|
||||
for(let i =0; i < refs.length; i++) {
|
||||
let r = parseInt(refs[i]);
|
||||
|
||||
const tag = props.note.post.tags[r];
|
||||
if (
|
||||
tag[0] === 'e' &&
|
||||
props.note.mentionedNotes &&
|
||||
props.note.mentionedNotes[tag[1]]
|
||||
) {
|
||||
const embeded = (
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
t.mentionIndication,
|
||||
{ name: userName(props.note.user) },
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
parsed = parsed.replace(`#[${r}]`, embeded.outerHTML);
|
||||
}
|
||||
|
||||
if (tag[0] === 'p' && props.mentionedUsers && props.mentionedUsers[tag[1]]) {
|
||||
const user = props.mentionedUsers[tag[1]];
|
||||
|
||||
const link = (
|
||||
<span class='linkish'>
|
||||
@{userName(user)}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
parsed = parsed.replace(`#[${r}]`, link.outerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
|
||||
};
|
||||
|
||||
const highlightHashtags = (text: string) => {
|
||||
const regex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/ig;
|
||||
|
||||
return text.replace(regex, (token) => {
|
||||
const [space, term] = token.split('#');
|
||||
const embeded = (
|
||||
<span>
|
||||
{space}
|
||||
<span class="linkish">#{term}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
return embeded.outerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
const wrapper = (children: JSXElement) => {
|
||||
if (props.includeEmbeds) {
|
||||
return (
|
||||
@ -167,21 +94,8 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class={styles.noteContent} innerHTML={
|
||||
parseNoteLinks(
|
||||
parseNpubLinks(
|
||||
parsedContent(
|
||||
highlightHashtags(
|
||||
parseNote2(props.note.post.content, media?.actions.getMediaUrl)
|
||||
),
|
||||
),
|
||||
props.note,
|
||||
'links',
|
||||
),
|
||||
props.note,
|
||||
!props.includeEmbeds,
|
||||
)
|
||||
}>
|
||||
<div class={styles.noteContent}>
|
||||
<ParsedNote note={props.note} ignoreMentionedNotes={true} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Component, JSX } from "solid-js";
|
||||
import { Component, createEffect, JSX, onCleanup, onMount } from "solid-js";
|
||||
import styles from "./NoteImage.module.scss";
|
||||
import mediumZoom from "medium-zoom";
|
||||
import type { Zoom } from 'medium-zoom';
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { generatePrivateKey } from "nostr-tools";
|
||||
|
||||
@ -10,9 +12,51 @@ const NoteImage: Component<{
|
||||
}> = (props) => {
|
||||
const imgId = generatePrivateKey();
|
||||
|
||||
const imgRef = () => {
|
||||
return document.getElementById(imgId)
|
||||
};
|
||||
|
||||
let zoomRef: Zoom | undefined;
|
||||
|
||||
const klass = () => `${styles.noteImage} ${props.isDev ? 'redBorder' : ''}`;
|
||||
|
||||
return <img id={imgId} src={props.src} class={klass()} onerror={props.onError} />;
|
||||
const doZoom = (e: MouseEvent) => {
|
||||
if (!e.target || (e.target as HTMLImageElement).id !== imgId) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
zoomRef?.open();
|
||||
};
|
||||
|
||||
const getZoom = () => {
|
||||
const iRef = imgRef();
|
||||
if (zoomRef || !iRef) {
|
||||
return zoomRef;
|
||||
}
|
||||
|
||||
zoomRef = mediumZoom(iRef, {
|
||||
background: "var(--background-site)",
|
||||
});
|
||||
|
||||
zoomRef.attach(iRef);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getZoom();
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
const iRef = imgRef();
|
||||
iRef && zoomRef && zoomRef.detach(iRef);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img id={imgId} src={props.src} class={klass()} onerror={props.onError} onClick={doZoom} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoteImage;
|
||||
|
569
src/components/PargingToken/ParsingToken.tsx
Normal file
569
src/components/PargingToken/ParsingToken.tsx
Normal file
@ -0,0 +1,569 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { hexToNpub } from '../../lib/keys';
|
||||
import {
|
||||
addLinkPreviews,
|
||||
isAppleMusic,
|
||||
isHashtag,
|
||||
isImage,
|
||||
isInterpunction,
|
||||
isLinebreak,
|
||||
isMixCloud,
|
||||
isMp4Video,
|
||||
isNostrNests,
|
||||
isNoteMention,
|
||||
isOggVideo,
|
||||
isSoundCloud,
|
||||
isSpotify,
|
||||
isTagMention,
|
||||
isTwitch,
|
||||
isUrl,
|
||||
isUserMention,
|
||||
isWavelake,
|
||||
isWebmVideo,
|
||||
isYouTube,
|
||||
} from '../../lib/notes';
|
||||
import { convertToUser, truncateNpub, userName } from '../../stores/profile';
|
||||
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
|
||||
import {
|
||||
Component, createEffect, createSignal, For, JSXElement, Match, onMount, Show, Switch,
|
||||
} from 'solid-js';
|
||||
import {
|
||||
MediaSize,
|
||||
NostrMentionContent,
|
||||
NostrNoteContent,
|
||||
NostrPostStats,
|
||||
NostrStatsContent,
|
||||
NostrUserContent,
|
||||
NoteReference,
|
||||
PrimalNote,
|
||||
PrimalUser,
|
||||
UserReference,
|
||||
} from '../../types/primal';
|
||||
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import LinkPreview from '../LinkPreview/LinkPreview';
|
||||
import MentionedUserLink from '../Note/MentionedUserLink/MentionedUserLink';
|
||||
import { useMediaContext } from '../../contexts/MediaContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { getMediaUrl, getMediaUrl as getMediaUrlDefault } from "../../lib/media";
|
||||
import NoteImage from '../NoteImage/NoteImage';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Kind, linebreakRegex } from '../../constants';
|
||||
import { APP_ID } from '../../App';
|
||||
import { getEvents } from '../../lib/feed';
|
||||
import { getUserProfileInfo } from '../../lib/profile';
|
||||
import { store, updateStore } from '../../services/StoreService';
|
||||
import { subscribeTo } from '../../sockets';
|
||||
import { convertToNotes } from '../../stores/note';
|
||||
import { account } from '../../translations';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
|
||||
|
||||
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 [store, updateStore] = createStore<ParserContextStore>({
|
||||
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 = subscribeTo(subId, (type, _, content) => {
|
||||
if (type === 'EOSE') {
|
||||
updateStore('isDataFetched', () => true)
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) return;
|
||||
|
||||
if (content.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
|
||||
const u = convertToUser(user);
|
||||
|
||||
updateStore('userRefs', () => ({ [u.pubkey]: u }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getUserProfileInfo(hex, account?.publicKey, subId);
|
||||
}
|
||||
catch (e) {
|
||||
console.log('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<string, NostrUserContent> = {};
|
||||
let messages: NostrNoteContent[] = [];
|
||||
let noteStats: NostrPostStats = {};
|
||||
let noteMentions: Record<string, NostrNoteContent> = {};
|
||||
|
||||
const unsub = subscribeTo(subId, (type, subId, content) =>{
|
||||
if (type === 'EOSE') {
|
||||
const newNote = convertToNotes({
|
||||
users,
|
||||
messages,
|
||||
postStats: noteStats,
|
||||
mentions: noteMentions,
|
||||
noteActions: {},
|
||||
})[0];
|
||||
|
||||
updateStore('noteRefs', () => ({[newNote.post.id]: { ...newNote }}));
|
||||
updateStore('isDataFetched', () => true)
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
getEvents(account?.publicKey, [hex], subId, true);
|
||||
}
|
||||
catch (e) {
|
||||
console.log('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', () => <a link href={url} target="_blank" >{url}</a>);
|
||||
|
||||
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', () => <div class="bordered"><LinkPreview preview={preview} /></div>);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 <NoteImage src={url} isDev={dev} />;
|
||||
}
|
||||
|
||||
const renderVideo = (token: string, type: string) => {
|
||||
return <video class="w-max" controls><source src={token} type={`video/${type}`} /></video>
|
||||
}
|
||||
|
||||
const renderYouTube = (token: string) => {
|
||||
const youtubeId = isYouTube(token) && RegExp.$1;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
class="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
// @ts-ignore no property
|
||||
key={youtubeId}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
|
||||
const renderSpotify = (token: string) => {
|
||||
const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
||||
|
||||
return (
|
||||
<iframe
|
||||
style="borderRadius: 12"
|
||||
src={convertedUrl}
|
||||
width="100%"
|
||||
height="352"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTwitch = (token: string) => {
|
||||
const channel = token.split("/").slice(-1);
|
||||
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={`https://player.twitch.tv/${args}`}
|
||||
// @ts-ignore no property
|
||||
className="w-max"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMixCloud = (token: string) => {
|
||||
const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
title="SoundCloud player"
|
||||
width="100%"
|
||||
height="120"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=%2F${feedPath}%2F`}
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSoundCloud = (token: string) => {
|
||||
return (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="166"
|
||||
// @ts-ignore no property
|
||||
scrolling="no"
|
||||
allow="autoplay"
|
||||
src={`https://w.soundcloud.com/player/?url=${token}`}
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAppleMusic = (token: string) => {
|
||||
const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com");
|
||||
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
height={`${isSongLink ? 175 : 450}`}
|
||||
style="width: 100%; maxWidth: 660; overflow: hidden; background: transparent;"
|
||||
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
|
||||
src={convertedUrl}
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWavelake = (token: string) => {
|
||||
const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||
|
||||
return (
|
||||
<iframe
|
||||
style="borderRadius: 12"
|
||||
src={convertedUrl}
|
||||
width="100%"
|
||||
height="380"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
);
|
||||
};
|
||||
|
||||
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 <><span class="linkish">{nostr}:{noteId}</span>{lastChar}</>;
|
||||
}
|
||||
|
||||
if (!props.noLinks) {
|
||||
const ment = store.noteRefs && store.noteRefs[hex];
|
||||
|
||||
return ment ?
|
||||
<>
|
||||
<EmbeddedNote
|
||||
note={ment}
|
||||
mentionedUsers={store.userRefs || {}}
|
||||
includeEmbeds={true}
|
||||
/>
|
||||
{lastChar}
|
||||
</> :
|
||||
<><A href={path}>{nostr}:{noteId}</A>{lastChar}</>;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('Failed to render note mention: ', e)
|
||||
return <span class="error">{token}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
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 = `/p/${npub}`;
|
||||
|
||||
let user = store.userRefs && store.userRefs[hex];
|
||||
|
||||
const label = user ? userName(user) : truncateNpub(npub);
|
||||
|
||||
if (props.noLinks === 'links') {
|
||||
return <><span class="linkish">@{label}</span>{lastChar}</>;
|
||||
}
|
||||
|
||||
if (!props.noLinks) {
|
||||
return !user ? <><A href={path}>@{label}</A>{lastChar}</> : <>{MentionedUserLink({ user })}{lastChar}</>;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('Failed to parse user mention: ', e)
|
||||
return <span class="error">{token}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch fallback={renderText(props.token)}>
|
||||
<Match when={props.token === '<_space_>'}>
|
||||
<> </>
|
||||
</Match>
|
||||
|
||||
<Match when={isUrl(props.token)}>
|
||||
<Switch fallback={store.renderedUrl}>
|
||||
|
||||
<Match when={isImage(props.token) && !props.ignoreMedia}>
|
||||
{renderImage(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isMp4Video(props.token) && !props.ignoreMedia}>
|
||||
{renderVideo(props.token, 'mp4')}
|
||||
</Match>
|
||||
|
||||
<Match when={isOggVideo(props.token) && !props.ignoreMedia}>
|
||||
{renderVideo(props.token, 'ogg')}
|
||||
</Match>
|
||||
|
||||
<Match when={isWebmVideo(props.token) && !props.ignoreMedia}>
|
||||
{renderVideo(props.token, 'webm')}
|
||||
</Match>
|
||||
|
||||
<Match when={isYouTube(props.token) && !props.ignoreMedia}>
|
||||
{renderYouTube(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isSpotify(props.token) && !props.ignoreMedia}>
|
||||
{renderSpotify(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isTwitch(props.token) && !props.ignoreMedia}>
|
||||
{renderTwitch(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isMixCloud(props.token) && !props.ignoreMedia}>
|
||||
{renderMixCloud(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isSoundCloud(props.token) && !props.ignoreMedia}>
|
||||
{renderSoundCloud(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isAppleMusic(props.token) && !props.ignoreMedia}>
|
||||
{renderAppleMusic(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isWavelake(props.token) && !props.ignoreMedia}>
|
||||
{renderWavelake(props.token)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
|
||||
<Match when={isLinebreak(props.token)}>
|
||||
<br/>
|
||||
</Match>
|
||||
|
||||
<Match when={isInterpunction(props.token)}>
|
||||
{renderText(props.token)}
|
||||
</Match>
|
||||
|
||||
<Match when={isNoteMention(props.token)}>
|
||||
<Show when={store.isDataFetched}>
|
||||
{renderNoteMention(props.token)}
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
<Match when={isUserMention(props.token)}>
|
||||
<Show when={store.isDataFetched}>
|
||||
{renderUserMention(props.token)}
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParsingToken;
|
@ -1,12 +1,32 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { hexToNpub } from '../../lib/keys';
|
||||
import { linkPreviews, parseNote1 } from '../../lib/notes';
|
||||
import {
|
||||
addLinkPreviews,
|
||||
isAppleMusic,
|
||||
isHashtag,
|
||||
isImage,
|
||||
isInterpunction,
|
||||
isMixCloud,
|
||||
isMp4Video,
|
||||
isNoteMention,
|
||||
isOggVideo,
|
||||
isSoundCloud,
|
||||
isSpotify,
|
||||
isTagMention,
|
||||
isTwitch,
|
||||
isUrl,
|
||||
isUserMention,
|
||||
isWavelake,
|
||||
isWebmVideo,
|
||||
isYouTube,
|
||||
} from '../../lib/notes';
|
||||
import { truncateNpub, userName } from '../../stores/profile';
|
||||
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
|
||||
import {
|
||||
Component, createEffect, createSignal,
|
||||
Component, createEffect, For, JSXElement, onMount, Show,
|
||||
} from 'solid-js';
|
||||
import {
|
||||
PrimalLinkPreview,
|
||||
PrimalNote,
|
||||
} from '../../types/primal';
|
||||
|
||||
@ -16,124 +36,378 @@ import LinkPreview from '../LinkPreview/LinkPreview';
|
||||
import MentionedUserLink from '../Note/MentionedUserLink/MentionedUserLink';
|
||||
import { useMediaContext } from '../../contexts/MediaContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { getMediaUrl as getMediaUrlDefault } from "../../lib/media";
|
||||
import NoteImage from '../NoteImage/NoteImage';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { linebreakRegex } from '../../constants';
|
||||
|
||||
|
||||
export const parseNoteLinks = (text: string, note: PrimalNote, highlightOnly?: 'text' | 'links') => {
|
||||
const regex = /\bnostr:((note|nevent)1\w+)\b|#\[(\d+)\]/g;
|
||||
|
||||
return text.replace(regex, (url) => {
|
||||
const [_, id] = url.split(':');
|
||||
|
||||
if (!id) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
const eventId = nip19.decode(id).data as string | nip19.EventPointer;
|
||||
const hex = typeof eventId === 'string' ? eventId : eventId.id;
|
||||
const noteId = nip19.noteEncode(hex);
|
||||
|
||||
const path = `/e/${noteId}`;
|
||||
|
||||
let link = <span>{url}</span>;
|
||||
|
||||
if (highlightOnly === 'links') {
|
||||
link = <span class='linkish'>@{url}</span>;
|
||||
}
|
||||
|
||||
if (!highlightOnly) {
|
||||
const ment = note.mentionedNotes && note.mentionedNotes[hex];
|
||||
|
||||
link = ment ?
|
||||
<div>
|
||||
<EmbeddedNote
|
||||
note={ment}
|
||||
mentionedUsers={note.mentionedUsers || {}}
|
||||
/>
|
||||
</div> :
|
||||
<A href={path}>{url}</A>;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return link.outerHTML || url;
|
||||
} catch (e) {
|
||||
return `<span class="${styles.error}">${url}</span>`;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
export const parseNpubLinks = (text: string, note: PrimalNote, highlightOnly?: 'links' | 'text') => {
|
||||
|
||||
const regex = /\bnostr:((npub|nprofile)1\w+)\b|#\[(\d+)\]/g;
|
||||
|
||||
return text.replace(regex, (url) => {
|
||||
const [_, id] = url.split(':');
|
||||
|
||||
if (!id) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileId = nip19.decode(id).data as string | nip19.ProfilePointer;
|
||||
|
||||
const hex = typeof profileId === 'string' ? profileId : profileId.pubkey;
|
||||
const npub = hexToNpub(hex);
|
||||
|
||||
const path = `/p/${npub}`;
|
||||
|
||||
const user = note.mentionedUsers && note.mentionedUsers[hex];
|
||||
|
||||
const label = user ? userName(user) : truncateNpub(npub);
|
||||
|
||||
let link = <span>@{label}</span>;
|
||||
|
||||
if (highlightOnly === 'links') {
|
||||
link = <span class='linkish'>@{label}</span>;
|
||||
}
|
||||
|
||||
if (!highlightOnly) {
|
||||
link = user ? <A href={path}>@{label}</A> : MentionedUserLink({ user });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return link.outerHTML || url;
|
||||
} catch (e) {
|
||||
return `<span class="${styles.error}">${url}</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const ParsedNote: Component<{
|
||||
note: PrimalNote,
|
||||
ignoreMentionedNotes?: boolean,
|
||||
id?: string,
|
||||
ignoreMedia?: boolean,
|
||||
noLinks?: 'links' | 'text',
|
||||
noPreviews?: boolean,
|
||||
}> = (props) => {
|
||||
|
||||
const media = useMediaContext();
|
||||
|
||||
const parsedContent = (text: string, highlightOnly?: 'text' | 'links') => {
|
||||
const regex = /\#\[([0-9]*)\]/g;
|
||||
let parsed = text;
|
||||
const [tokens, setTokens] = createStore<string[]>([])
|
||||
const [renderedUrl, setRenderedUrl] = createStore<Record<string, any>>({});
|
||||
|
||||
let refs = [];
|
||||
let match;
|
||||
const parseContent = () => {
|
||||
const content = props.note.post.content.replace(linebreakRegex, ' __LB__ ').replace(/\s+/g, ' __SP__ ');
|
||||
const tokens = content.split(/[\s]+/);
|
||||
|
||||
while((match = regex.exec(text)) !== null) {
|
||||
refs.push(match[1]);
|
||||
}
|
||||
setTokens(() => [...tokens]);
|
||||
}
|
||||
|
||||
if (refs.length > 0) {
|
||||
for(let i =0; i < refs.length; i++) {
|
||||
let r = parseInt(refs[i]);
|
||||
const parseToken: (token: string) => JSXElement = (token: string) => {
|
||||
|
||||
if (token === '__LB__') {
|
||||
// setElements(elements.length, <br />);
|
||||
return <br />;
|
||||
}
|
||||
if (token === '__SP__') {
|
||||
// setElements(elements.length, <br />);
|
||||
return <> </>;
|
||||
}
|
||||
|
||||
if (isInterpunction(token)) {
|
||||
// setElements(elements.length, <span>{token}</span>)
|
||||
return <span>{token}</span>;
|
||||
}
|
||||
|
||||
if (isUrl(token)) {
|
||||
const index = token.indexOf('http');
|
||||
|
||||
if (index > 0) {
|
||||
const prefix = token.slice(0, index);
|
||||
const url = token.slice(index);
|
||||
// tokens.splice(i+1, 0, prefix);
|
||||
// tokens.splice(i+2, 0, url);
|
||||
return <>{parseToken(prefix)} {parseToken(url)}</>;
|
||||
}
|
||||
|
||||
if (!props.ignoreMedia) {
|
||||
if (isImage(token)) {
|
||||
const dev = localStorage.getItem('devMode') === 'true';
|
||||
let imgUrl = media?.actions.getMediaUrl(token);
|
||||
const url = imgUrl || getMediaUrlDefault(token)
|
||||
|
||||
// setElements(elements.length, <NoteImage src={url} isDev={dev} />);
|
||||
return <NoteImage src={url} isDev={dev} />;
|
||||
}
|
||||
|
||||
if (isMp4Video(token)) {
|
||||
// setElements(elements.length, <video class="w-max" controls><source src={token} type="video/mp4" /></video>);
|
||||
return <video class="w-max" controls><source src={token} type="video/mp4" /></video>;
|
||||
}
|
||||
|
||||
if (isOggVideo(token)) {
|
||||
// setElements(elements.length, <video class="w-max" controls><source src={token} type="video/ogg" /></video>);
|
||||
return <video class="w-max" controls><source src={token} type="video/ogg" /></video>;
|
||||
}
|
||||
|
||||
if (isWebmVideo(token)) {
|
||||
// setElements(elements.length, <video class="w-max" controls><source src={token} type="video/webm" /></video>);
|
||||
return <video class="w-max" controls><source src={token} type="video/webm" /></video>;
|
||||
}
|
||||
|
||||
if (isYouTube(token)) {
|
||||
const youtubeId = isYouTube(token) && RegExp.$1;
|
||||
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// class="w-max"
|
||||
// src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
// title="YouTube video player"
|
||||
// // @ts-ignore no property
|
||||
// key={youtubeId}
|
||||
// frameBorder="0"
|
||||
// allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
// allowFullScreen
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
class="w-max"
|
||||
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||
title="YouTube video player"
|
||||
// @ts-ignore no property
|
||||
key={youtubeId}
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
if (isSpotify(token)) {
|
||||
const convertedUrl = token.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
||||
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// style="borderRadius: 12"
|
||||
// src={convertedUrl}
|
||||
// width="100%"
|
||||
// height="352"
|
||||
// // @ts-ignore no property
|
||||
// frameBorder="0"
|
||||
// allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
// loading="lazy"
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
style="borderRadius: 12"
|
||||
src={convertedUrl}
|
||||
width="100%"
|
||||
height="352"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
if (isTwitch(token)) {
|
||||
const channel = token.split("/").slice(-1);
|
||||
|
||||
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
||||
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// src={`https://player.twitch.tv/${args}`}
|
||||
// // @ts-ignore no property
|
||||
// className="w-max"
|
||||
// allowFullScreen
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
src={`https://player.twitch.tv/${args}`}
|
||||
// @ts-ignore no property
|
||||
className="w-max"
|
||||
allowFullScreen
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
if (isMixCloud(token)) {
|
||||
const feedPath = (isMixCloud(token) && RegExp.$1) + "%2F" + (isMixCloud(token) && RegExp.$2);
|
||||
|
||||
// setElements(elements.length,
|
||||
// <div>
|
||||
// <iframe
|
||||
// title="SoundCloud player"
|
||||
// width="100%"
|
||||
// height="120"
|
||||
// // @ts-ignore no property
|
||||
// frameBorder="0"
|
||||
// src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=%2F${feedPath}%2F`}
|
||||
// ></iframe>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
return <div>
|
||||
<iframe
|
||||
title="SoundCloud player"
|
||||
width="100%"
|
||||
height="120"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=%2F${feedPath}%2F`}
|
||||
></iframe>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (isSoundCloud(token)) {
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// width="100%"
|
||||
// height="166"
|
||||
// // @ts-ignore no property
|
||||
// scrolling="no"
|
||||
// allow="autoplay"
|
||||
// src={`https://w.soundcloud.com/player/?url=${token}`}
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
width="100%"
|
||||
height="166"
|
||||
// @ts-ignore no property
|
||||
scrolling="no"
|
||||
allow="autoplay"
|
||||
src={`https://w.soundcloud.com/player/?url=${token}`}
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
if (isAppleMusic(token)) {
|
||||
const convertedUrl = token.replace("music.apple.com", "embed.music.apple.com");
|
||||
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
||||
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
|
||||
// // @ts-ignore no property
|
||||
// frameBorder="0"
|
||||
// height={`${isSongLink ? 175 : 450}`}
|
||||
// style="width: 100%; maxWidth: 660; overflow: hidden; background: transparent;"
|
||||
// sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
|
||||
// src={convertedUrl}
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
height={`${isSongLink ? 175 : 450}`}
|
||||
style="width: 100%; maxWidth: 660; overflow: hidden; background: transparent;"
|
||||
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
|
||||
src={convertedUrl}
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
if (isWavelake(token)) {
|
||||
const convertedUrl = token.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||
|
||||
// setElements(elements.length,
|
||||
// <iframe
|
||||
// style="borderRadius: 12"
|
||||
// src={convertedUrl}
|
||||
// width="100%"
|
||||
// height="380"
|
||||
// // @ts-ignore no property
|
||||
// frameBorder="0"
|
||||
// loading="lazy"
|
||||
// ></iframe>
|
||||
// );
|
||||
return <iframe
|
||||
style="borderRadius: 12"
|
||||
src={convertedUrl}
|
||||
width="100%"
|
||||
height="380"
|
||||
// @ts-ignore no property
|
||||
frameBorder="0"
|
||||
loading="lazy"
|
||||
></iframe>;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.noLinks === 'text') {
|
||||
// setElements(elements.length, <span class="whole">{token}</span>);
|
||||
return <span class="whole">{token}</span>;
|
||||
}
|
||||
|
||||
addLinkPreviews(token).then(preview => {
|
||||
replaceLink(token, preview);
|
||||
});
|
||||
|
||||
// setElements(elements.length, c);
|
||||
return <span data-url={token}><a link href={token} target="_blank" >{token}</a></span>;
|
||||
}
|
||||
|
||||
if (isNoteMention(token)) {
|
||||
const [_, id] = token.split(':');
|
||||
|
||||
if (!id) {
|
||||
return token;
|
||||
}
|
||||
|
||||
let link = <span>{token}</span>;
|
||||
|
||||
try {
|
||||
const eventId = nip19.decode(id).data as string | nip19.EventPointer;
|
||||
const hex = typeof eventId === 'string' ? eventId : eventId.id;
|
||||
const noteId = nip19.noteEncode(hex);
|
||||
|
||||
const path = `/e/${noteId}`;
|
||||
|
||||
if (props.noLinks === 'links') {
|
||||
link = <span class='linkish'>@{token}</span>;
|
||||
}
|
||||
|
||||
if (!props.noLinks) {
|
||||
const ment = props.note.mentionedNotes && props.note.mentionedNotes[hex];
|
||||
|
||||
link = ment ?
|
||||
<div>
|
||||
<EmbeddedNote
|
||||
note={ment}
|
||||
mentionedUsers={props.note.mentionedUsers || {}}
|
||||
/>
|
||||
</div> :
|
||||
<A href={path}>{token}</A>;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
link = <span class={styles.error}>{token}</span>;
|
||||
}
|
||||
|
||||
// setElements(elements.length, <span class="whole"> {link}</span>);
|
||||
return <span class="whole"> {link}</span>;
|
||||
}
|
||||
|
||||
if (isUserMention(token)) {
|
||||
let [_, id] = token.split(':');
|
||||
|
||||
if (!id) {
|
||||
return token;
|
||||
}
|
||||
|
||||
let end = id[id.length - 1];
|
||||
|
||||
if ([',', '?', ';', '!'].some(x => end === x)) {
|
||||
id = id.slice(0, -1);
|
||||
} else {
|
||||
end = '';
|
||||
}
|
||||
|
||||
try {
|
||||
const profileId = nip19.decode(id).data as string | nip19.ProfilePointer;
|
||||
|
||||
const hex = typeof profileId === 'string' ? profileId : profileId.pubkey;
|
||||
const npub = hexToNpub(hex);
|
||||
|
||||
const path = `/p/${npub}`;
|
||||
|
||||
let user = props.note.mentionedUsers && props.note.mentionedUsers[hex];
|
||||
|
||||
const label = user ? userName(user) : truncateNpub(npub);
|
||||
|
||||
let link = <span>@{label}{end}</span>;
|
||||
|
||||
if (props.noLinks === 'links') {
|
||||
link = <><span class='linkish'>@{label}</span>{end}</>;
|
||||
}
|
||||
|
||||
if (!props.noLinks) {
|
||||
link = !user ?
|
||||
<><A href={path}>@{label}</A>{end}</> :
|
||||
<>{MentionedUserLink({ user })}{end}</>;
|
||||
}
|
||||
|
||||
// setElements(elements.length, <span class="whole"> {link}</span>);
|
||||
return <span class="whole"> {link}</span>;
|
||||
} catch (e) {
|
||||
// setElements(elements.length, <span class={styles.error}> {token}</span>);
|
||||
return <span class={styles.error}> {token}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTagMention(token)) {
|
||||
let t = `${token}`;
|
||||
|
||||
|
||||
let end = t[t.length - 1];
|
||||
|
||||
if ([',', '?', ';', '!'].some(x => end === x)) {
|
||||
t = t.slice(0, -1);
|
||||
} else {
|
||||
end = '';
|
||||
}
|
||||
|
||||
let r = parseInt(t.slice(2, t.length - 1));
|
||||
|
||||
const tag = props.note.post.tags[r];
|
||||
|
||||
if (tag === undefined || tag.length === 0) continue;
|
||||
if (tag === undefined || tag.length === 0) return;
|
||||
|
||||
if (
|
||||
tag[0] === 'e' &&
|
||||
@ -144,13 +418,13 @@ const ParsedNote: Component<{
|
||||
const noteId = `nostr:${nip19.noteEncode(hex)}`;
|
||||
const path = `/e/${nip19.noteEncode(hex)}`;
|
||||
|
||||
let embeded = <span>{noteId}</span>;
|
||||
let embeded = <span>{noteId}{end}</span>;
|
||||
|
||||
if (highlightOnly === 'links') {
|
||||
embeded = <span class='linkish'>@{noteId}</span>;
|
||||
if (props.noLinks === 'links') {
|
||||
embeded = <><span class='linkish'>@{noteId}</span>{end}</>;
|
||||
}
|
||||
|
||||
if (!highlightOnly) {
|
||||
if (!props.noLinks) {
|
||||
const ment = props.note.mentionedNotes[hex];
|
||||
|
||||
embeded = ment ?
|
||||
@ -159,12 +433,13 @@ const ParsedNote: Component<{
|
||||
note={ment}
|
||||
mentionedUsers={props.note.mentionedUsers}
|
||||
/>
|
||||
{end}
|
||||
</div> :
|
||||
<A href={path}>{noteId}</A>;
|
||||
<><A href={path}>{noteId}</A>{end}</>;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
parsed = parsed.replace(`#[${r}]`, embeded.outerHTML);
|
||||
// setElements(elements.length, <span class="whole"> embeded</span>);
|
||||
return <span class="whole"> embeded</span>;
|
||||
}
|
||||
|
||||
if (tag[0] === 'p' && props.note.mentionedUsers && props.note.mentionedUsers[tag[1]]) {
|
||||
@ -174,121 +449,70 @@ const ParsedNote: Component<{
|
||||
|
||||
const label = userName(user);
|
||||
|
||||
let link = <span>@{label}</span>;
|
||||
let link = <span>@{label}{end}</span>;
|
||||
|
||||
if (highlightOnly === 'links') {
|
||||
link = <span class='linkish'>@{label}</span>;
|
||||
if (props.noLinks === 'links') {
|
||||
link = <><span class='linkish'>@{label}</span>{end}</>;
|
||||
}
|
||||
|
||||
if (!highlightOnly) {
|
||||
link = user ? <A href={path}>@{label}</A> : MentionedUserLink({ user });
|
||||
if (!props.noLinks) {
|
||||
link = user ?
|
||||
<><A href={path}>@{label}</A>{end}</> :
|
||||
<>{MentionedUserLink({ user })}{end}</>;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
parsed = parsed.replace(`#[${r}]`, link.outerHTML);
|
||||
// setElements(elements.length, <span> {link}</span>);
|
||||
return <span> {link}</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
if (isHashtag(token)) {
|
||||
const [_, term] = token.split('#');
|
||||
const embeded = props.noLinks === 'text' ?
|
||||
<span>#{term}</span> :
|
||||
<A href={`/search/%23${term}`}>#{term}</A>;
|
||||
|
||||
};
|
||||
|
||||
const highlightHashtags = (text: string, noLinks?: 'links' | 'text') => {
|
||||
const regex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/ig;
|
||||
|
||||
return text.replace(regex, (token) => {
|
||||
const [space, term] = token.split('#');
|
||||
const embeded = noLinks === 'text' ? (
|
||||
<span>
|
||||
{space}
|
||||
<span>#{term}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{space}
|
||||
<A
|
||||
href={`/search/%23${term}`}
|
||||
>#{term}</A>
|
||||
</span>
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
return embeded.outerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
const replaceLinkPreviews = (text: string, previews: Record<string, any>) => {
|
||||
let parsed = text;
|
||||
|
||||
const regex = /__LINK__.*?__LINK__/ig;
|
||||
|
||||
parsed = parsed.replace(regex, (link) => {
|
||||
const url = link.split('__LINK__')[1];
|
||||
|
||||
const preview = previews[url];
|
||||
|
||||
const hasMinimalPreviewData = preview && preview.url &&
|
||||
((preview.description && preview.description.length > 0) || preview.image || preview.title);
|
||||
|
||||
if (!hasMinimalPreviewData) {
|
||||
return `<a link href="${url}" target="_blank" >${url}</a>`;
|
||||
// setElements(elements.length, <span class="whole"> {embeded}</span>);
|
||||
return <span class="whole"> {embeded}</span>;
|
||||
}
|
||||
|
||||
const linkElement = (<div class={styles.bordered}><LinkPreview preview={preview} /></div>);
|
||||
// const c = <span class="whole">
|
||||
// <Show when={i > 0}> </Show>
|
||||
// {token}
|
||||
// </span>;
|
||||
|
||||
// @ts-ignore
|
||||
return linkElement.outerHTML;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const content = () => {
|
||||
return parseNoteLinks(
|
||||
parseNpubLinks(
|
||||
parsedContent(
|
||||
highlightHashtags(
|
||||
parseNote1(props.note.post.content, media?.actions.getMediaUrl)
|
||||
),
|
||||
props.noLinks,
|
||||
),
|
||||
props.note,
|
||||
props.noLinks,
|
||||
),
|
||||
props.note,
|
||||
props.noLinks,
|
||||
);
|
||||
// setElements(elements.length, c);
|
||||
return <span class="whole">{token}</span>;
|
||||
};
|
||||
|
||||
const smallContent = () => {
|
||||
return parseNoteLinks(
|
||||
parseNpubLinks(
|
||||
parsedContent(
|
||||
highlightHashtags(
|
||||
props.note.post.content,
|
||||
props.noLinks,
|
||||
),
|
||||
props.noLinks,
|
||||
),
|
||||
props.note,
|
||||
props.noLinks,
|
||||
),
|
||||
props.note,
|
||||
props.noLinks,
|
||||
);
|
||||
};
|
||||
|
||||
const [displayedContent, setDisplayedContent] = createSignal<string>(props.ignoreMedia ? smallContent() : content());
|
||||
|
||||
createEffect(() => {
|
||||
const newContent = replaceLinkPreviews(displayedContent(), { ...linkPreviews });
|
||||
|
||||
setDisplayedContent(() => newContent);
|
||||
onMount(() => {
|
||||
parseContent();
|
||||
});
|
||||
|
||||
let noteHolder: HTMLDivElement | undefined;
|
||||
|
||||
const replaceLink = (url: string, preview: PrimalLinkPreview) => {
|
||||
if (!noteHolder) return;
|
||||
|
||||
const hasMinimalPreviewData = !props.noPreviews &&
|
||||
preview &&
|
||||
preview.url &&
|
||||
((preview.description && preview.description.length > 0) ||
|
||||
preview.images ||
|
||||
preview.title
|
||||
);
|
||||
|
||||
if (hasMinimalPreviewData) {
|
||||
// @ts-ignore
|
||||
noteHolder.querySelector(`[data-url="${url}"]`).innerHTML = (<div class="bordered"><LinkPreview preview={preview} /></div>).outerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={props.id} innerHTML={displayedContent()}>
|
||||
<div id={props.id} ref={noteHolder}>
|
||||
<For each={tokens}>
|
||||
{(token) => <>{parseToken(token)}</>}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -230,9 +230,27 @@ export const notificationTypeNoteProps: Record<string, string> = {
|
||||
|
||||
}
|
||||
|
||||
export const noteRegex = /nostr:((note|nevent)1\w+)\b|#\[(\d+)\]/g;
|
||||
export const profileRegex = /nostr:((npub|nprofile)1\w+)\b|#\[(\d+)\]/g;
|
||||
export const editMentionRegex = /(?:\s|^)@\`(.*?)\`/ig;
|
||||
// export const odyseeRegex = /odysee\.com\/([a-zA-Z0-9]+)/;
|
||||
// export const magnetRegex = /(magnet:[\S]+)/i;
|
||||
// export const tweetUrlRegex = /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
|
||||
// export const tidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i;
|
||||
export const spotifyRegex = /open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/;
|
||||
export const twitchRegex = /twitch.tv\/([a-z0-9_]+$)/i;
|
||||
export const mixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
|
||||
export const soundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
|
||||
export const appleMusicRegex = /music\.apple\.com\/([a-z]{2}\/)?(?:album|playlist)\/[\w\d-]+\/([.a-zA-Z0-9-]+)(?:\?i=\d+)?/i;
|
||||
export const nostrNestsRegex = /nostrnests\.com\/[a-zA-Z0-9]+/i;
|
||||
export const wavlakeRegex = /(?:player\.)?wavlake\.com\/(track\/[.a-zA-Z0-9-]+|album\/[.a-zA-Z0-9-]+|[.a-zA-Z0-9-]+)/i;
|
||||
export const youtubeRegex = /(?:https?:\/\/)?(?:www|m\.)?(?:youtu\.be\/|youtube\.com\/(?:live\/|shorts\/|embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/;
|
||||
export const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9\u00F0-\u02AF@:%._\+~#=]{1,256}\.[a-zA-Z0-9\u00F0-\u02AF()]{1,8}\b([-a-zA-Z0-9\u00F0-\u02AF()@:%_\+.~#?&//=]*)/;
|
||||
export const interpunctionRegex = /^(\.|,|;|\?|\!)$/;
|
||||
|
||||
export const hashtagRegex = /(?:\s|^)#[^\s!@#$%^&*(),.?":{}|<>]+/i;
|
||||
export const linebreakRegex = /(\r\n|\r|\n)/ig;
|
||||
export const tagMentionRegex = /\#\[([0-9]*)\]/;
|
||||
export const noteRegex = /nostr:((note|nevent)1\w+)\b/;
|
||||
export const profileRegex = /nostr:((npub|nprofile)1\w+)\b/;
|
||||
export const editMentionRegex = /(?:\s|^)@\`(.*?)\`/i;
|
||||
|
||||
export const medZapLimit = 1000;
|
||||
|
||||
|
@ -398,10 +398,10 @@ body {
|
||||
}
|
||||
|
||||
.medium-zoom-image {
|
||||
cursor: pointer !important;
|
||||
// cursor: pointer !important;
|
||||
}
|
||||
.medium-zoom-overlay {
|
||||
cursor: pointer !important;
|
||||
// cursor: pointer !important;
|
||||
z-index: var(--z-index-header);
|
||||
}
|
||||
.medium-zoom-image-opened {
|
||||
|
@ -3,7 +3,7 @@ import { Relay } from "nostr-tools";
|
||||
import { createStore } from "solid-js/store";
|
||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
||||
import NoteImage from "../components/NoteImage/NoteImage";
|
||||
import { Kind } from "../constants";
|
||||
import { appleMusicRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, mixCloudRegex, nostrNestsRegex, noteRegex, profileRegex, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, wavlakeRegex, youtubeRegex } from "../constants";
|
||||
import { sendMessage, subscribeTo } from "../sockets";
|
||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { getMediaUrl as getMediaUrlDefault } from "./media";
|
||||
@ -47,20 +47,13 @@ export const addLinkPreviews = async (url: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const spotifyRegex = /open\.spotify\.com\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/;
|
||||
export const twitchRegex = /twitch.tv\/([a-z0-9_]+$)/i;
|
||||
export const mixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
|
||||
// export const tidalRegex = /tidal\.com\/(?:browse\/)?(\w+)\/([a-z0-9-]+)/i;
|
||||
export const soundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/;
|
||||
// export const tweetUrlRegex = /https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
|
||||
export const appleMusicRegex = /music\.apple\.com\/([a-z]{2}\/)?(?:album|playlist)\/[\w\d-]+\/([.a-zA-Z0-9-]+)(?:\?i=\d+)?/i;
|
||||
export const nostrNestsRegex = /nostrnests\.com\/[a-zA-Z0-9]+/i;
|
||||
// export const magnetRegex = /(magnet:[\S]+)/i;
|
||||
export const wavlakeRegex = /(?:player\.)?wavlake\.com\/(track\/[.a-zA-Z0-9-]+|album\/[.a-zA-Z0-9-]+|[.a-zA-Z0-9-]+)/i;
|
||||
// export const odyseeRegex = /odysee\.com\/([a-zA-Z0-9]+)/;
|
||||
export const youtubeRegex = /(?:https?:\/\/)?(?:www|m\.)?(?:youtu\.be\/|youtube\.com\/(?:live\/|shorts\/|embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})/;
|
||||
export const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9\u00F0-\u02AF@:%._\+~#=]{1,256}\.[a-zA-Z0-9\u00F0-\u02AF()]{1,8}\b([-a-zA-Z0-9\u00F0-\u02AF()@:%_\+.~#?&//=]*)/g;
|
||||
|
||||
export const isUrl = (url: string) => urlRegex.test(url);
|
||||
export const isHashtag = (url: string) => hashtagRegex.test(url);
|
||||
export const isLinebreak = (url: string) => linebreakRegex.test(url);
|
||||
export const isTagMention = (url: string) => tagMentionRegex.test(url);
|
||||
export const isNoteMention = (url: string) => noteRegex.test(url);
|
||||
export const isUserMention = (url: string) => profileRegex.test(url);
|
||||
export const isInterpunction = (url: string) => interpunctionRegex.test(url);
|
||||
|
||||
export const isImage = (url: string) => ['.jpg', '.jpeg', '.webp', '.png', '.gif', '.format=png'].some(x => url.includes(x));
|
||||
export const isMp4Video = (url: string) => ['.mp4', '.mov'].some(x => url.includes(x));
|
||||
|
@ -193,7 +193,6 @@ const Thread: Component = () => {
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
30
src/types/primal.d.ts
vendored
30
src/types/primal.d.ts
vendored
@ -491,27 +491,8 @@ export type ExploreFeedPayload = {
|
||||
created_after?: number,
|
||||
}
|
||||
|
||||
export type UserReference = {
|
||||
id: string,
|
||||
pubkey: string,
|
||||
kind: number,
|
||||
tags: string[][],
|
||||
npub?: string,
|
||||
name?: string,
|
||||
about?: string,
|
||||
picture?: string,
|
||||
nip05?: string,
|
||||
banner?: string,
|
||||
display_name?: string,
|
||||
displayName?: string,
|
||||
location?: string,
|
||||
lud06?: string,
|
||||
lud16?: string,
|
||||
website?: string,
|
||||
content?: string,
|
||||
created_at?: number,
|
||||
sig?: string,
|
||||
};
|
||||
export type UserReference = Record<string, PrimalUser>;
|
||||
export type NoteReference = Record<string, PrimalNote>;
|
||||
|
||||
export type ContextChildren =
|
||||
number |
|
||||
@ -520,6 +501,13 @@ export type ContextChildren =
|
||||
JSX.ArrayElement |
|
||||
(string & {}) | null | undefined;
|
||||
|
||||
export type PrimalLinkPreview = {
|
||||
url: string,
|
||||
description?: string,
|
||||
title?: string,
|
||||
images?: string[],
|
||||
favicons?: string[],
|
||||
};
|
||||
|
||||
export type PrimalTheme = { name: string, label: string, logo: string, dark?: boolean};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user