mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Parse profile about
This commit is contained in:
parent
1638029c93
commit
98835daf54
@ -48,6 +48,11 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
|
||||
const groupGridLimit = 7;
|
||||
|
||||
export type NoteContent = {
|
||||
type: string,
|
||||
tokens: string[],
|
||||
meta?: Record<string, any>,
|
||||
};
|
||||
|
||||
export const groupGalleryImages = (noteHolder: HTMLDivElement | undefined) => {
|
||||
|
||||
@ -195,12 +200,6 @@ const ParsedNote: Component<{
|
||||
setTokens(() => [...tokens]);
|
||||
}
|
||||
|
||||
type NoteContent = {
|
||||
type: string,
|
||||
tokens: string[],
|
||||
meta?: Record<string, any>,
|
||||
};
|
||||
|
||||
const removeLinebreaks = () => {
|
||||
if (lastSignificantContent === 'LB') {
|
||||
const lastIndex = content.length - 1;
|
||||
|
285
src/components/ProfileAbout/ProfileAbout.tsx
Normal file
285
src/components/ProfileAbout/ProfileAbout.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Component, createEffect, For, JSXElement, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { APP_ID } from '../../App';
|
||||
import { linebreakRegex, urlExtractRegex, specialCharsRegex, hashtagCharsRegex, profileRegexG, Kind } from '../../constants';
|
||||
import { hexToNpub, npubToHex } from '../../lib/keys';
|
||||
import { isInterpunction, isUrl, isUserMention, isHashtag } from '../../lib/notes';
|
||||
import { getUserProfiles } from '../../lib/profile';
|
||||
import { subscribeTo } from '../../sockets';
|
||||
import { userName, truncateNpub } from '../../stores/profile';
|
||||
import { NostrUserContent } from '../../types/primal';
|
||||
import MentionedUserLink from '../Note/MentionedUserLink/MentionedUserLink';
|
||||
import { NoteContent } from '../ParsedNote/ParsedNote';
|
||||
|
||||
import styles from '../../pages/Profile.module.scss';
|
||||
|
||||
const ProfileAbout: Component<{about: string | undefined }> = (props) => {
|
||||
|
||||
const [usersMentionedInAbout, setUsersMentionedInAbout] = createStore<Record<string, any>>({});
|
||||
|
||||
const [aboutTokens, setAboutTokens] = createStore<string[]>([]);
|
||||
|
||||
const [aboutContent, setAboutContent] = createStore<NoteContent[]>([]);
|
||||
|
||||
let lastSignificantContent = 'text';
|
||||
|
||||
const tokenizeAbout = (about: string) => {
|
||||
const content = about.replace(linebreakRegex, ' __LB__ ').replace(/\s+/g, ' __SP__ ');
|
||||
const tokens = content.split(/[\s]+/);
|
||||
|
||||
setAboutTokens(() => [...tokens]);
|
||||
}
|
||||
|
||||
const updateAboutContent = (type: string, token: string, meta?: Record<string, any>) => {
|
||||
setAboutContent((contentArray) => {
|
||||
if (contentArray.length > 0 && contentArray[contentArray.length -1].type === type) {
|
||||
const c = { ...contentArray[contentArray.length - 1] };
|
||||
|
||||
c.tokens = [...c.tokens, token];
|
||||
|
||||
if (meta) {
|
||||
c.meta = { ...meta };
|
||||
}
|
||||
|
||||
return [ ...contentArray.slice(0, contentArray.length - 1), { ...c }];
|
||||
}
|
||||
|
||||
return [...contentArray, { type, tokens: [token], meta: { ...meta } }]
|
||||
});
|
||||
}
|
||||
|
||||
const parseAboutToken = (token: string) => {
|
||||
if (token === '__LB__') {
|
||||
|
||||
updateAboutContent('linebreak', token);
|
||||
lastSignificantContent = 'LB';
|
||||
return;
|
||||
}
|
||||
|
||||
if (token === '__SP__') {
|
||||
if (!['LB'].includes(lastSignificantContent)) {
|
||||
updateAboutContent('text', ' ');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInterpunction(token)) {
|
||||
lastSignificantContent = 'text';
|
||||
updateAboutContent('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);
|
||||
|
||||
parseAboutToken(prefix);
|
||||
parseAboutToken(matched);
|
||||
parseAboutToken(suffix);
|
||||
return;
|
||||
} else {
|
||||
parseAboutToken(prefix);
|
||||
parseAboutToken(token.slice(index));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lastSignificantContent = 'link';
|
||||
updateAboutContent('link', token);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUserMention(token)) {
|
||||
lastSignificantContent = 'usermention';
|
||||
updateAboutContent('usermention', token);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHashtag(token)) {
|
||||
lastSignificantContent = 'hashtag';
|
||||
updateAboutContent('hashtag', token);
|
||||
return;
|
||||
}
|
||||
|
||||
lastSignificantContent = 'text';
|
||||
updateAboutContent('text', token);
|
||||
return;
|
||||
};
|
||||
|
||||
const renderLinebreak = (item: NoteContent) => {
|
||||
|
||||
// Allow max consecutive linebreak
|
||||
const len = Math.min(2, item.tokens.length);
|
||||
|
||||
const lineBreaks = Array(len).fill(<br/>)
|
||||
|
||||
return <For each={lineBreaks}>{_ => <br/>}</For>
|
||||
};
|
||||
|
||||
const renderText = (item: NoteContent) => {
|
||||
let tokens = [];
|
||||
|
||||
for (let i=0;i<item.tokens.length;i++) {
|
||||
const token = item.tokens[i];
|
||||
|
||||
tokens.push(token)
|
||||
}
|
||||
|
||||
const text = tokens.join(' ').replaceAll('<', '<').replaceAll('>', '>');
|
||||
|
||||
return <>{text}</>;
|
||||
};
|
||||
|
||||
const renderLinks = (item: NoteContent, index?: number) => {
|
||||
return <For each={item.tokens}>
|
||||
{(token) => {
|
||||
return <span data-url={token}><a link href={token} target="_blank" >{token}</a></span>;
|
||||
}}
|
||||
</For>
|
||||
};
|
||||
|
||||
const renderUserMention = (item: NoteContent) => {
|
||||
return <For each={item.tokens}>
|
||||
{(token) => {
|
||||
let [_, id] = token.split(':');
|
||||
|
||||
if (!id) {
|
||||
return <>{token}</>;
|
||||
}
|
||||
|
||||
let end = '';
|
||||
|
||||
let match = specialCharsRegex.exec(id);
|
||||
|
||||
if (match) {
|
||||
const i = match.index;
|
||||
end = id.slice(i);
|
||||
id = id.slice(0, i);
|
||||
}
|
||||
|
||||
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 = usersMentionedInAbout && usersMentionedInAbout[hex];
|
||||
|
||||
const label = user ? userName(user) : truncateNpub(npub);
|
||||
|
||||
return !user ?
|
||||
<><A href={path}>@{label}</A>{end}</> :
|
||||
<>{MentionedUserLink({ user })}{end}</>;
|
||||
} catch (e) {
|
||||
return <span class={styles.error}> {token}</span>;
|
||||
}
|
||||
}}
|
||||
</For>
|
||||
};
|
||||
|
||||
const renderHashtag = (item: NoteContent) => {
|
||||
return <For each={item.tokens}>
|
||||
{(token) => {
|
||||
let [_, term] = token.split('#');
|
||||
let end = '';
|
||||
|
||||
let match = hashtagCharsRegex.exec(term);
|
||||
|
||||
if (match) {
|
||||
const i = match.index;
|
||||
end = term.slice(i);
|
||||
term = term.slice(0, i);
|
||||
}
|
||||
|
||||
const embeded = <A href={`/search/%23${term}`}>#{term}</A>;
|
||||
|
||||
return <span class="whole"> {embeded}{end}</span>;
|
||||
}}
|
||||
</For>
|
||||
};
|
||||
|
||||
const renderAboutContent = (item: NoteContent, index: number) => {
|
||||
|
||||
const renderers: Record<string, (item: NoteContent, index?: number) => JSXElement> = {
|
||||
linebreak: renderLinebreak,
|
||||
text: renderText,
|
||||
link: renderLinks,
|
||||
usermention: renderUserMention,
|
||||
hashtag: renderHashtag,
|
||||
}
|
||||
|
||||
return renderers[item.type] ?
|
||||
renderers[item.type](item, index) :
|
||||
<></>;
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (aboutTokens.length === 0) return;
|
||||
|
||||
for (let i=0; i < aboutTokens.length; i++) {
|
||||
parseAboutToken(aboutTokens[i]);
|
||||
}
|
||||
});
|
||||
|
||||
const parseForMentions = (about: string) => {
|
||||
let userMentions = [];
|
||||
let m;
|
||||
|
||||
do {
|
||||
m = profileRegexG.exec(about);
|
||||
if (m) {
|
||||
userMentions.push(npubToHex(m[1]))
|
||||
}
|
||||
} while (m);
|
||||
|
||||
const subId = `pa_u_${APP_ID}`;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, _, content) => {
|
||||
if (type === 'EVENT' && content?.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
const profile = JSON.parse(user.content);
|
||||
|
||||
setUsersMentionedInAbout(() => ({[user.pubkey]: ({ ...profile })}));
|
||||
}
|
||||
|
||||
if(type === 'EOSE') {
|
||||
unsub();
|
||||
tokenizeAbout(about);
|
||||
}
|
||||
});
|
||||
|
||||
getUserProfiles(userMentions, subId);
|
||||
|
||||
// const a = linkifyNostrNoteLink(linkifyNostrProfileLink(urlify(sanitize(about), () => '', false, false, true)));
|
||||
|
||||
// setRenderProfileAbout(a)
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (props.about && props.about.length > 0) {
|
||||
parseForMentions(props.about);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Show when={aboutContent.length > 0}>
|
||||
<div class={styles.profileAbout}>
|
||||
<For each={aboutContent}>
|
||||
{(item, index) => renderAboutContent(item, index())}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileAbout;
|
@ -1,5 +1,5 @@
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { A } from "@solidjs/router";
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { Relay } from "nostr-tools";
|
||||
import { createStore } from "solid-js/store";
|
||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RouteDataFuncArgs, useNavigate, useParams, useRouteData } from '@solidjs/router';
|
||||
import { A, RouteDataFuncArgs, useNavigate, useParams, useRouteData } from '@solidjs/router';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import {
|
||||
Component,
|
||||
@ -19,7 +19,7 @@ import { useProfileContext } from '../contexts/ProfileContext';
|
||||
import { useAccountContext } from '../contexts/AccountContext';
|
||||
import Wormhole from '../components/Wormhole/Wormhole';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { urlify, sanitize, linkifyNostrProfileLink, linkifyNostrNoteLink } from '../lib/notes';
|
||||
import { sanitize } from '../lib/notes';
|
||||
import { shortDate } from '../lib/dates';
|
||||
|
||||
import styles from './Profile.module.scss';
|
||||
@ -43,6 +43,7 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import NoteImage from '../components/NoteImage/NoteImage';
|
||||
import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeModal';
|
||||
import { CustomZapInfo, useAppContext } from '../contexts/AppContext';
|
||||
import ProfileAbout from '../components/ProfileAbout/ProfileAbout';
|
||||
|
||||
const Profile: Component = () => {
|
||||
|
||||
@ -436,18 +437,6 @@ const Profile: Component = () => {
|
||||
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorNpubCoppied));
|
||||
};
|
||||
|
||||
const [renderProfileAbout, setRenderProfileAbout] = createSignal('');
|
||||
|
||||
const getProfileAbout = (about: string) => {
|
||||
const a = linkifyNostrNoteLink(linkifyNostrProfileLink(urlify(sanitize(about), () => '', false, false, true)));
|
||||
|
||||
setRenderProfileAbout(a)
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
getProfileAbout(profile?.userProfile?.about || '');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (showContext()) {
|
||||
document.addEventListener('click', onClickOutside);
|
||||
@ -664,10 +653,7 @@ const Profile: Component = () => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={renderProfileAbout().length > 0}>
|
||||
<div class={styles.profileAbout} innerHTML={renderProfileAbout()}>
|
||||
</div>
|
||||
</Show>
|
||||
<ProfileAbout about={profile?.userProfile?.about} />
|
||||
|
||||
|
||||
<Show when={profile?.userProfile?.website}>
|
||||
|
Loading…
Reference in New Issue
Block a user