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;
|
const groupGridLimit = 7;
|
||||||
|
|
||||||
|
export type NoteContent = {
|
||||||
|
type: string,
|
||||||
|
tokens: string[],
|
||||||
|
meta?: Record<string, any>,
|
||||||
|
};
|
||||||
|
|
||||||
export const groupGalleryImages = (noteHolder: HTMLDivElement | undefined) => {
|
export const groupGalleryImages = (noteHolder: HTMLDivElement | undefined) => {
|
||||||
|
|
||||||
@ -195,12 +200,6 @@ const ParsedNote: Component<{
|
|||||||
setTokens(() => [...tokens]);
|
setTokens(() => [...tokens]);
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoteContent = {
|
|
||||||
type: string,
|
|
||||||
tokens: string[],
|
|
||||||
meta?: Record<string, any>,
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeLinebreaks = () => {
|
const removeLinebreaks = () => {
|
||||||
if (lastSignificantContent === 'LB') {
|
if (lastSignificantContent === 'LB') {
|
||||||
const lastIndex = content.length - 1;
|
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";
|
import { A } from "@solidjs/router";
|
||||||
|
// @ts-ignore Bad types in nostr-tools
|
||||||
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";
|
||||||
|
@ -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 { nip19 } from 'nostr-tools';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@ -19,7 +19,7 @@ import { useProfileContext } from '../contexts/ProfileContext';
|
|||||||
import { useAccountContext } from '../contexts/AccountContext';
|
import { useAccountContext } from '../contexts/AccountContext';
|
||||||
import Wormhole from '../components/Wormhole/Wormhole';
|
import Wormhole from '../components/Wormhole/Wormhole';
|
||||||
import { useIntl } from '@cookbook/solid-intl';
|
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 { shortDate } from '../lib/dates';
|
||||||
|
|
||||||
import styles from './Profile.module.scss';
|
import styles from './Profile.module.scss';
|
||||||
@ -43,6 +43,7 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
|||||||
import NoteImage from '../components/NoteImage/NoteImage';
|
import NoteImage from '../components/NoteImage/NoteImage';
|
||||||
import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeModal';
|
import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeModal';
|
||||||
import { CustomZapInfo, useAppContext } from '../contexts/AppContext';
|
import { CustomZapInfo, useAppContext } from '../contexts/AppContext';
|
||||||
|
import ProfileAbout from '../components/ProfileAbout/ProfileAbout';
|
||||||
|
|
||||||
const Profile: Component = () => {
|
const Profile: Component = () => {
|
||||||
|
|
||||||
@ -436,18 +437,6 @@ const Profile: Component = () => {
|
|||||||
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorNpubCoppied));
|
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(() => {
|
createEffect(() => {
|
||||||
if (showContext()) {
|
if (showContext()) {
|
||||||
document.addEventListener('click', onClickOutside);
|
document.addEventListener('click', onClickOutside);
|
||||||
@ -664,10 +653,7 @@ const Profile: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={renderProfileAbout().length > 0}>
|
<ProfileAbout about={profile?.userProfile?.about} />
|
||||||
<div class={styles.profileAbout} innerHTML={renderProfileAbout()}>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
|
|
||||||
<Show when={profile?.userProfile?.website}>
|
<Show when={profile?.userProfile?.website}>
|
||||||
|
Loading…
Reference in New Issue
Block a user