wip separate files for different embed types

This commit is contained in:
Martti Malmi 2023-08-05 23:57:10 +03:00
parent 375066f7ee
commit 9bf0c04692
34 changed files with 416 additions and 26 deletions

View File

@ -306,7 +306,7 @@ export default {
});
}
// Spotify album
// SpotifyTrack album
if (settings.enableSpotify !== false) {
const spotifyRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/album\/)([\w-]+)(?:\S+)?/g;
@ -327,7 +327,7 @@ export default {
});
}
// Spotify playlist
// SpotifyTrack playlist
if (settings.enableSpotify !== false) {
const spotifyPlaylistRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/playlist\/)([\w-]+)(?:\S+)?/g;

View File

@ -0,0 +1,32 @@
import { memo } from 'react';
import reactStringReplace from 'react-string-replace';
import { Event } from 'nostr-tools';
import { allEmbeds, textEmbeds } from './embed';
const HyperText = memo(
({ children, event, textOnly }: { children: string; event?: Event; textOnly?: boolean }) => {
let processedChildren = [children.trim()];
const embeds = textOnly ? textEmbeds : allEmbeds;
embeds.forEach((embed) => {
processedChildren = reactStringReplace(processedChildren, embed.regex, (match, i) => {
return embed.component({
match,
index: i,
event,
key: `${match}-${i}`,
});
});
});
processedChildren = processedChildren.map((x) =>
typeof x === 'string' ? x.replace(/^\n+|\n+$/g, '') : x,
);
return <>{processedChildren}</>;
},
);
export default HyperText;

View File

@ -10,6 +10,7 @@ import { DecryptedEvent } from '../views/chat/ChatMessages';
import Name from './user/Name';
import Torrent from './Torrent';
import HyperText from "./HyperText";
type Props = {
event: DecryptedEvent;
@ -81,7 +82,6 @@ const PrivateMessage = ({ event, selfAuthored, showName, torrentId }: Props) =>
};
const emojiOnly = text && text.length === 2 && Helpers.isEmoji(text);
const formattedText = Helpers.highlightEverything(text || '');
// TODO opts.onImageClick show image in modal
const time =
@ -108,7 +108,7 @@ const PrivateMessage = ({ event, selfAuthored, showName, torrentId }: Props) =>
</div>
{torrentId && <Torrent torrentId={torrentId} />}
<div className={`preformatted-wrap text-base ${emojiOnly ? 'text-4xl' : ''}`}>
{formattedText}
<HyperText event={event}>{text}</HyperText>
</div>
<div className={`${selfAuthored ? 'text-right' : 'text-left'} text-xs text-white`}>
{event.id ? Helpers.getRelativeTimeText(time) : Helpers.formatTime(time)}

View File

View File

View File

View File

@ -0,0 +1,17 @@
import { Link } from 'preact-router';
import Embed from './index';
const Hashtag: Embed = {
regex: /(?<=\s|^)(#\w+)/g,
component: ({ match, key }) => {
return (
<Link key={key} href={`/search/${encodeURIComponent(match)}`} className="link">
{' '}
{match}{' '}
</Link>
);
},
};
export default Hashtag;

View File

@ -0,0 +1,36 @@
import { useState } from 'react';
import Modal from '../modal/Modal';
import SafeImg from '../SafeImg';
import Embed from './index';
import Show from "../helpers/Show";
const Image: Embed = {
regex: /(https?:\/\/.*\.(?:png|jpg|jpeg|gif|svg|webp)(?:\?\S*)?)/gi,
component: ({ match, key }) => {
const [showModal, setShowModal] = useState(false);
const onClick = (e) => {
e.stopPropagation();
setShowModal(true);
};
return (
<div key={key}>
<div className="relative w-full overflow-hidden object-contain my-2">
<SafeImg
onClick={onClick}
className="rounded max-h-[70vh] md:max-h-96 max-w-full cursor-pointer"
src={match}
/>
</div>
<Show when={showModal}>
<Modal onClose={() => setShowModal(false)}>
<SafeImg className="rounded max-h-[90vh] max-w-[90vw]" src={match} />
</Modal>
</Show>
</div>
);
},
};
export default Image;

View File

@ -0,0 +1,22 @@
import Embed from './index';
const Instagram: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:instagram\.com\/)((?:p|reel)\/[\w-]{11})(?:\S+)?/g,
component: ({ match, key }) => {
return (
<iframe
className="instagram"
key={key}
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://instagram.com/${match}/embed`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default Instagram;

View File

View File

@ -0,0 +1,23 @@
import Embed from './index';
const SoundCloud: Embed = {
regex:
/(?:https?:\/\/)?(?:www\.)?(soundcloud\.com\/(?!live)[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)(?:\?.*)?/g,
component: ({ match, key }) => {
return (
<iframe
key={key}
className="audio"
scrolling="no"
width="650"
height="380"
style={{ maxWidth: '100%' }}
src={`https://w.soundcloud.com/player/?url=${match}`}
frameBorder="0"
allow="encrypted-media"
/>
);
},
};
export default SoundCloud;

View File

View File

View File

View File

View File

@ -0,0 +1,24 @@
import Embed from './index';
const Twitter: Embed = {
regex: /(?:^|\s)(?:@)?(https?:\/\/twitter.com\/\w+\/status\/\d+\S*)(?![\w/])/g,
component: ({ match, key }) => {
return (
<iframe
style={{
maxWidth: '350px',
height: '450px',
backgroundColor: 'white',
display: 'block',
}}
key={key}
scrolling="no"
height={250}
width={550}
src={`https://twitframe.com/show?url=${encodeURIComponent(match)}`}
/>
);
},
};
export default Twitter;

View File

@ -0,0 +1,17 @@
import { Link } from 'preact-router';
import Embed from './index';
const Url: Embed = {
regex: /(https?:\/\/[^\s]+)/g,
component: ({ match, key }) => {
const url = match.replace(/^(https:\/\/)?iris.to/, '');
return (
<Link key={key} className="link" target="_blank" href={url}>
{match.replace(/^https?:\/\//, '').replace(/\/$/, '')}
</Link>
);
},
};
export default Url;

View File

@ -0,0 +1,20 @@
import Embed from './index';
const Video: Embed = {
regex: /(https?:\/\/.*\.(?:mp4|webm|ogg|mov)(?:\?\S*)?)/gi,
component: ({ match, key }) => (
<div key={key} className="relative w-full overflow-hidden object-contain my-2">
<video
className="rounded max-h-[70vh] md:max-h-96 max-w-full"
src={match}
controls
muted
autoPlay
loop
poster={`https://imgproxy.iris.to/thumbnail/638/${match}`}
></video>
</div>
),
};
export default Video;

View File

View File

@ -0,0 +1,21 @@
import Embed from './index';
const YouTube: Embed = {
regex:
/(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))([\w-]{11})(?:\S+)?/g,
component: ({ match, key }) => {
return (
<iframe
key={key}
width="650"
height="400"
src={`https://www.youtube.com/embed/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default YouTube;

View File

@ -0,0 +1,48 @@
import { Event } from 'nostr-tools';
import { JSX } from 'preact';
import InlineMention from './nostr/InlineMention';
import Nip19 from './nostr/Nip19';
import NostrEvent from './nostr/NostrNote';
import NostrNpub from './nostr/NostrNpub';
import SpotifyTrack from './spotify/SpotifyTrack';
import Hashtag from './Hashtag';
import Image from './Image';
import Instagram from './Instagram';
import SoundCloud from './SoundCloud';
import Twitter from './Twitter';
import Url from './Url';
import Video from './Video';
import Youtube from './YouTube';
export type EmbedProps = {
match: string;
index?: number;
event?: Event;
key: string;
};
type Embed = {
regex: RegExp;
component: (props: EmbedProps) => JSX.Element;
};
export const allEmbeds = [
Image,
Video,
Youtube,
Instagram,
Twitter,
SoundCloud,
SpotifyTrack,
NostrNpub,
NostrEvent,
Nip19,
Hashtag,
InlineMention,
Url,
];
export const textEmbeds = [NostrNpub, Url, Hashtag];
export default Embed;

View File

@ -0,0 +1,41 @@
// mentions like #[3], can refer to event or user
import { nip19 } from 'nostr-tools';
import { Link } from 'preact-router';
import EventComponent from '../../events/EventComponent';
import Name from '../../user/Name';
import Embed from '../index';
const fail = (s: string) => `#[${s}]`;
const InlineMention: Embed = {
regex: /#\[([0-9]+)]/g,
component: ({ match, index, event, key }) => {
if (!event?.tags) {
console.log('no tags', event);
return <>{fail(match)}</>;
}
const tag = event.tags[parseInt(match)];
if (!tag) {
console.log('no matching tag', index, event);
return <>{fail(match)}</>;
}
const [type, id] = tag;
if (type === 'p') {
return (
<Link key={key} href={`/${nip19.npubEncode(id)}`} className="link">
<Name pub={id} hideBadge={true} />
</Link>
);
} else if (type === 'e') {
return <EventComponent id={id} key={id} asInlineQuote={true} />;
} else {
console.log('unknown tag type', type, index, event);
return <>{fail(match)}</>;
}
},
};
export default InlineMention;

View File

@ -0,0 +1,40 @@
import { nip19 } from 'nostr-tools';
import { Link } from 'preact-router';
import EventComponent from '../../events/EventComponent';
import Name from '../../user/Name';
import Embed from '../index';
const nip19Regex = /\bnostr:(n(?:event|profile)1\w+)\b/g;
const NostrUser: Embed = {
regex: nip19Regex,
component: ({ match, key }) => {
try {
const { type, data } = nip19.decode(match);
if (type === 'nprofile') {
return (
<>
{' '}
<Link key={key} className="text-iris-blue hover:underline" href={`/${data.pubkey}`}>
<Name pub={data.pubkey} />
</Link>
</>
);
} else if (type === 'nevent') {
// same as note
return (
<div key={key} className="rounded-lg border border-gray-500 my-2">
<EventComponent id={data.id} asInlineQuote={true} />
</div>
);
}
} catch (e) {
console.log(e);
}
return <span key={key}>{match}</span>;
},
};
export default NostrUser;

View File

@ -0,0 +1,17 @@
import Key from '../../../nostr/Key';
import EventComponent from '../../events/EventComponent';
import Embed from '../index';
const eventRegex =
/(?:^|\s|nostr:|(?:https?:\/\/[\w./]+)|iris\.to\/|snort\.social\/e\/|damus\.io\/)+((?:@)?note[a-zA-Z0-9]{59,60})(?![\w/])/gi;
const NostrUser: Embed = {
regex: eventRegex,
component: ({ match, key }) => {
const hex = Key.toNostrHexAddress(match.replace('@', ''))!;
return <EventComponent key={key} id={hex} asInlineQuote={true} />;
},
};
export default NostrUser;

View File

@ -0,0 +1,22 @@
import { Link } from 'preact-router';
import Name from '../../user/Name';
import Embed from '../index';
const pubKeyRegex =
/(?:^|\s|nostr:|(?:https?:\/\/[\w./]+)|iris\.to\/|snort\.social\/p\/|damus\.io\/)+((?:@)?npub[a-zA-Z0-9]{59,60})(?![\w/])/gi;
const NostrNpub: Embed = {
regex: pubKeyRegex,
component: ({ match, key }) => {
const pub = match.replace('@', '');
return (
<Link key={key} href={`/${pub}`} className="link mr-1">
<Name pub={pub} hideBadge={true} />
</Link>
);
},
};
export default NostrNpub;

View File

@ -0,0 +1,23 @@
import Embed from '../index';
const SpotifyTrack: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/track\/)([\w-]+)(?:\S+)?/g,
component: ({ match, key }) => {
return (
<iframe
className="audio"
scrolling="no"
key={key}
width="650"
height="200"
style={{ maxWidth: '100%' }}
src={`https://open.spotify.com/embed/track/${match}?utm_source=oembed`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default SpotifyTrack;

View File

@ -6,7 +6,7 @@ import localState from '../../../LocalState';
import SocialNetwork from '../../../nostr/SocialNetwork';
import { translate as t } from '../../../translations/Translation.mjs';
import Show from '../../helpers/Show';
import ImageModal from '../../modal/Image';
import HyperText from '../../HyperText';
import PublicMessageForm from '../../PublicMessageForm';
import Torrent from '../../Torrent';
import Reactions from '../buttons/ReactionButtons';
@ -32,7 +32,6 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
const [translatedText, setTranslatedText] = useState('');
const [showMore, setShowMore] = useState(false);
const [name, setName] = useState('');
const [showImageModal, setShowImageModal] = useState(false);
useEffect(() => {
if (standalone) {
@ -74,16 +73,6 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
? `${lines.slice(0, MSG_TRUNCATE_LINES).join('\n')}...`
: text;
text = Helpers.highlightEverything(text.trim(), event, {
showMentionedMessages: !asInlineQuote,
onImageClick: (e) => imageClicked(e),
});
function imageClicked(e) {
e.preventDefault();
setShowImageModal(true);
}
function isTooLong() {
return (
attachments?.length > 1 ||
@ -109,7 +98,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
</Show>
<Show when={text?.length > 0}>
<div className={`preformatted-wrap pb-1 ${emojiOnly && 'text-2xl'}`}>
{text}
<HyperText event={event}>{text}</HyperText>
<Show when={translatedText}>
<p>
<i>{translatedText}</i>
@ -148,12 +137,6 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, meta })
placeholder={t('write_your_reply')}
/>
</Show>
<Show when={showImageModal}>
<ImageModal
images={attachments?.map((a) => a.data)}
onClose={() => setShowImageModal(false)}
/>
</Show>
</div>
);
};

View File

@ -30,7 +30,6 @@ const Overlay = styled.div<Props>`
const ModalContentContainer = styled.div<{ width?: string; height?: string }>`
width: ${(props) => props.width || 'auto'};
height: ${(props) => props.height || 'auto'};
max-height: calc(100% - 40px);
overflow-y: auto;
display: flex;
flex-direction: column;

View File

@ -17,6 +17,7 @@ import ProfileDropdown from './Dropdown';
import Name from './Name';
import ProfilePicture from './ProfilePicture';
import Stats from './Stats';
import HyperText from "../HyperText";
const ProfileCard = (props: { hexPub: string; npub: string }) => {
const { hexPub, npub } = props;
@ -197,7 +198,9 @@ const ProfileCard = (props: { hexPub: string; npub: string }) => {
</div>
<Stats address={hexPub} />
<div className="py-2">
<p className="text-sm">{profile.about}</p>
<p className="text-sm">
<HyperText textOnly={true}>{profile.about.slice(0, 500)}</HyperText>
</p>
<div className="flex flex-1 flex-row align-center justify-center mt-4">
<Show when={lightning}>
<div className="flex-1">

View File

@ -171,7 +171,9 @@ function ChatMessages({ id }) {
/>
</div>
<Show when={showQr}>
<QrCode data={'nostr:' + formatPrivateKey()} />
<div className="mt-4">
<QrCode data={'nostr:' + formatPrivateKey()} />
</div>
</Show>
</Show>
<Show when={!isGroup && Key.toNostrHexAddress(id) !== myPub}>