embed system

This commit is contained in:
Martti Malmi 2023-08-06 08:25:10 +03:00
parent 9bf0c04692
commit 467e37e621
35 changed files with 339 additions and 579 deletions

View File

@ -1,17 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import reactStringReplace from 'react-string-replace';
import { bech32 } from 'bech32';
import $ from 'jquery';
import _ from 'lodash';
import throttle from 'lodash/throttle';
import { nip19 } from 'nostr-tools';
import { ComponentChild } from 'preact';
import { Link, route } from 'preact-router';
import EventComponent from './components/events/EventComponent';
import SafeImg from './components/SafeImg';
import Torrent from './components/Torrent';
import Name from './components/user/Name';
import Key from './nostr/Key';
import { language, translate as t } from './translations/Translation.mjs';
import localState from './LocalState';
@ -22,14 +14,10 @@ const pubKeyRegex =
/(?:^|\s|nostr:|(?:https?:\/\/[\w./]+)|iris\.to\/|snort\.social\/p\/|damus\.io\/)+((?:@)?npub[a-zA-Z0-9]{59,60})(?![\w/])/gi;
const noteRegex =
/(?:^|\s|nostr:|(?:https?:\/\/[\w./]+)|iris\.to\/|snort\.social\/e\/|damus\.io\/)+((?:@)?note[a-zA-Z0-9]{59,60})(?![\w/])/gi;
const nip19Regex = /\bnostr:(n(?:event|profile)1\w+)\b/g;
const hashtagRegex = /(#[^\s!@#$%^&*()=+./,[{\]};:'"?><]+)/g;
let settings: any = {};
localState.get('settings').on((s) => (settings = s));
let existingIrisToAddress: any = {};
localState.get('settings').put({}); // ?
localState.get('existingIrisToAddress').on((a) => (existingIrisToAddress = a));
const userAgent = navigator.userAgent.toLowerCase();
@ -124,419 +112,6 @@ export default {
window.open(link, '_self');
},
highlightEverything(s: string, event?: any, opts: any = { showMentionedMessages: true }): any[] {
let replacedText = reactStringReplace(s, emojiRegex, (match, i) => {
return (
<span key={match + i} className="emoji">
{match}
</span>
);
});
if (opts.showMentionedMessages) {
replacedText = reactStringReplace(replacedText, noteRegex, (match, i) => {
return (
<EventComponent
key={match + i}
id={Key.toNostrHexAddress(match) || ''}
asInlineQuote={true}
/>
);
});
}
if (settings.enableTwitter !== false) {
const twitterRegex = /(?:^|\s)(?:@)?(https?:\/\/twitter.com\/\w+\/status\/\d+\S*)(?![\w/])/g;
replacedText = reactStringReplace(replacedText, twitterRegex, (match, i) => {
return (
<iframe
style={{
'max-width': '350px',
height: '450px',
'background-color': 'white',
display: 'block',
}}
key={match + i}
scrolling="no"
height={250}
width={550}
src={`https://twitframe.com/show?url=${encodeURIComponent(match)}`}
/>
);
});
}
if (settings.enableVideos !== false) {
const videoRegex =
/(https?:\/\/[^?\s]+\/[^?\s]+\.(?:mp4|mkv|avi|flv|wmv|mov|webm)(?:\?\S*)?)\b/gi;
replacedText = reactStringReplace(replacedText, videoRegex, (match, i) => {
return (
<video
className="py-2 rounded max-h-[70vh] md:max-h-96 max-w-full"
key={match + i}
src={match}
poster={`https://imgproxy.iris.to/thumbnail/428/${match}`}
muted={!this.isMobile && settings.autoplayVideos !== false}
autoPlay={!this.isMobile && settings.autoplayVideos !== false}
playsInline
controls
loop
onLoadedData={(e) => {
if (!this.isMobile && settings.autoplayVideos) {
(e.target as HTMLVideoElement).play();
}
}}
/>
);
});
}
if (settings.enableAudio !== false) {
const audioRegex = /(https?:\/\/\S+\.(?:mp3|wav|ogg|flac))\b/gi;
replacedText = reactStringReplace(replacedText, audioRegex, (match, i) => {
return <audio key={match + i} src={match} controls={true} loop={true} />;
});
}
if (settings.enableYoutube !== false) {
const youtubeRegex =
/(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))([\w-]{11})(?:\S+)?/g;
replacedText = reactStringReplace(replacedText, youtubeRegex, (match, i) => {
return (
<iframe
key={match + i}
width="650"
height="400"
src={`https://www.youtube.com/embed/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
if (settings.enableInstagram !== false) {
const igRegex =
/(?:https?:\/\/)?(?:www\.)?(?:instagram\.com\/)((?:p|reel)\/[\w-]{11})(?:\S+)?/g;
replacedText = reactStringReplace(replacedText, igRegex, (match, i) => {
return (
<iframe
class="instagram"
key={match + i}
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
/>
);
});
}
// Soundcloud
if (settings.enableSoundCloud !== false) {
const soundCloudRegex =
/(?:https?:\/\/)?(?:www\.)?(soundcloud\.com\/(?!live)[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)(?:\?.*)?/g;
replacedText = reactStringReplace(replacedText, soundCloudRegex, (match, i) => {
return (
console.log('match: ' + match),
console.log('match 0: ' + match[0]),
console.log('match 1: ' + match[1]),
console.log('match 2: ' + match[2]),
(
<iframe
class="audio"
scrolling="no"
key={match + i}
width="650"
height="380"
style={{ maxWidth: '100%' }}
src={`https://w.soundcloud.com/player/?url=${match}`}
frameBorder="0"
allow="encrypted-media"
/>
)
);
});
}
if (settings.enableSpotify !== false) {
const spotifyRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/track\/)([\w-]+)(?:\S+)?/g;
replacedText = reactStringReplace(replacedText, spotifyRegex, (match, i) => {
return (
<iframe
class="audio"
scrolling="no"
key={match + i}
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
/>
);
});
}
//spotify podcast episode
if (settings.enableSpotify !== false) {
const spotifyRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/episode\/)([\w-]+)(?:\S+)?(?:t=(\d+))?/g;
replacedText = reactStringReplace(replacedText, spotifyRegex, (match, i) => {
return (
<iframe
class="audio"
scrolling="no"
key={match + i}
width="650"
height="200"
style={{ maxWidth: '100%' }}
src={`https://open.spotify.com/embed/episode/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// SpotifyTrack album
if (settings.enableSpotify !== false) {
const spotifyRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/album\/)([\w-]+)(?:\S+)?/g;
replacedText = reactStringReplace(replacedText, spotifyRegex, (match, i) => {
return (
<iframe
class="audio"
scrolling="no"
key={match + i}
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://open.spotify.com/embed/album/${match}`}
frameBorder="0"
allow="encrypted-media"
/>
);
});
}
// SpotifyTrack playlist
if (settings.enableSpotify !== false) {
const spotifyPlaylistRegex =
/(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/playlist\/)([\w-]+)(?:\S+)?/g;
replacedText = reactStringReplace(replacedText, spotifyPlaylistRegex, (match, i) => {
return (
<iframe
class="audio"
scrolling="no"
key={match + i}
width="650"
height="380"
style={{ maxWidth: '100%' }}
src={`https://open.spotify.com/embed/playlist/${match}`}
frameBorder="0"
allow="encrypted-media"
/>
);
});
}
// Apple Music
if (settings.enableAppleMusic !== false) {
const appleMusicRegex = /(?:https?:\/\/)(?:.*?)(music\.apple\.com\/.*)/gi;
replacedText = reactStringReplace(replacedText, appleMusicRegex, (match, i) => {
return (
<iframe
class="applemusic"
scrolling="no"
key={match + i}
width="650"
height="150"
style={{ maxWidth: '100%' }}
src={`https://embed.music.apple.com/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// Apple Podcast
if (settings.enableAppleMusic !== false) {
const applePodcastRegex = /(?:https?:\/\/)?(?:www\.)?(podcasts\.apple\.com\/.*)/g;
replacedText = reactStringReplace(replacedText, applePodcastRegex, (match, i) => {
console.log('embed url: ' + match);
const cssClass = match.includes('?i=') ? 'applepodcast-small' : 'applepodcast-large';
return (
<iframe
// class="applepodcast"
class={cssClass}
scrolling="no"
key={match + i}
width="650"
height="175"
style={{ maxWidth: '100%' }}
src={`https://embed.${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
if (settings.enableTidal !== false) {
const tidalRegex = /(?:https?:\/\/)?(?:www\.)?(?:tidal\.com(?:\/browse)?\/track\/)([\d]+)?/g;
replacedText = reactStringReplace(replacedText, tidalRegex, (match, i) => {
return (
<iframe
class="audio"
scrolling="no"
key={match + i}
width="650"
height="200"
style={{ maxWidth: '100%' }}
src={`https://embed.tidal.com/tracks/${match}?layout=gridify`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// Tiktok embed
if (settings.enableTiktok !== false) {
const tiktokRegex = /(?:https?:\/\/)?(?:www\.)?tiktok\.com\/.*?video\/(\d{1,19})/g;
replacedText = reactStringReplace(replacedText, tiktokRegex, (match, i) => {
return (
<iframe
class="tiktok"
width="605"
height="400"
key={match + i}
style={{ maxWidth: '100%' }}
src={`https://www.tiktok.com/embed/v2/${match}`}
frameBorder="1"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// twitch.com/videos
if (settings.enableTwitch !== false) {
const twitchRegex = /(?:https?:\/\/)?(?:www\.)?(?:twitch\.tv\/videos\/)([\d]+)?/g;
replacedText = reactStringReplace(replacedText, twitchRegex, (match, i) => {
return (
<iframe
class="video"
scrolling="no"
key={match + i}
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://player.twitch.tv/?video=${match}&parent=${window.location.hostname}&autoplay=false`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// twitch channels
if (settings.enableTwitch !== false) {
const twitchRegex = /(?:https?:\/\/)?(?:www\.)?(?:twitch\.tv\/)([\w-]+)?/g;
replacedText = reactStringReplace(replacedText, twitchRegex, (match, i) => {
return (
<iframe
class="video"
scrolling="no"
key={match + i}
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://player.twitch.tv/?channel=${match}&parent=${window.location.hostname}&autoplay=false`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
});
}
// wavlake track/album/artist
if (settings.enableWavlake !== false) {
const wavlakeRegex =
/https:\/\/(?:player\.)?wavlake\.com\/(?!feed\/|artists)(track\/[.a-zA-Z0-9-]+|album\/[.a-zA-Z0-9-]+|[.a-zA-Z0-9-]+)/i;
replacedText = reactStringReplace(replacedText, wavlakeRegex, (match, i) => {
return (
<iframe
key={match + i}
height="380"
width="100%"
style={{ maxWidth: '100%' }}
src={`https://embed.wavlake.com/${match}`}
frameBorder="0"
loading="lazy"
/>
);
});
}
if (settings.enableTorrent !== false) {
const magnetRegex = /(magnet:\?xt=urn:btih:.*)/gi;
replacedText = reactStringReplace(replacedText, magnetRegex, (match, i) => {
// Torrent component
console.log('magnet link', match);
return <Torrent key={match + i} preview={true} torrentId={match} />;
});
}
// find .jpg .jpeg .gif .png .webp urls in msg.text and create img tag
if (settings.enableImages !== false) {
const imgRegex = /(https?:\/\/[^\s]*\.(?:jpg|jpeg|gif|png|webp)(\?[^\s]*)?( |$|\n))/gi;
replacedText = reactStringReplace(replacedText, imgRegex, (match, i) => {
return (
<SafeImg
className="py-2 md:rounded max-h-[70vh] md:max-h-96 max-w-full cursor-pointer"
onClick={opts.onImageClick}
src={match}
key={match + i}
/>
);
});
}
replacedText = this.highlightText(replacedText, event, opts);
const lnRegex =
/(lightning:[\w.-]+@[\w.-]+|lightning:\w+\?amount=\d+|(?:lightning:)?(?:lnurl|lnbc)[\da-z0-9]+)/gi;
replacedText = reactStringReplace(replacedText, lnRegex, (match) => {
if (!match.startsWith('lightning:')) {
match = `lightning:${match}`;
}
return (
<a href={match} onClick={(e) => this.handleLightningLinkClick(e)}>
Pay with lightning
</a>
);
});
return replacedText;
},
isMobile: (function () {
let check = false;
(function (a) {
@ -553,125 +128,6 @@ export default {
return check;
})(),
// hashtags, usernames, links
highlightText(s: ComponentChild[], event?: any, opts: any = {}) {
s = reactStringReplace(s, pubKeyRegex, (match, i) => {
match = match.replace(/@/g, '');
const link = `/${match}`;
return (
<>
{' '}
<a href={link} className="link">
@<Name key={match + i} pub={match} hideBadge={true} />
</a>
</>
);
});
// nip19 decode
s = reactStringReplace(s, nip19Regex, (match, i) => {
try {
const { type, data } = nip19.decode(match);
if (type === 'nprofile') {
return (
<>
{' '}
<Link href={`/${data.pubkey}`} className="link">
@<Name key={match + i} pub={data.pubkey} hideBadge={true} />
</Link>
</>
);
} else if (type === 'nevent') {
// same as note
return <EventComponent key={match + i} id={data.id} asInlineQuote={true} />;
}
} catch (e) {
console.log(e);
return match;
}
});
s = reactStringReplace(s, noteRegex, (match) => {
match = match.replace(/@/g, '');
const link = `/${match}`;
return (
<>
{' '}
<a href={link} className="link">
{match}
</a>
</>
);
});
s = reactStringReplace(
s,
/((?:https?:\/\/\S*[^.?,)\s])|(?:iris\.to\/\S*[^.?,)\s]))/gi,
(match, i) => {
const url = match.replace(/^(https:\/\/)?iris.to/, '');
const isIris = url !== match;
return (
<a
key={match + i}
className="link"
target="_blank"
onClick={(e) => {
if (isIris) {
e.preventDefault();
route(url);
}
}}
href={url}
>
{match.replace(/^https?:\/\//, '').replace(/\/$/, '')}
</a>
);
},
);
if (event?.tags) {
// replace "#[n]" tags with links to the user: event.tags[n][1]
s = reactStringReplace(s, /#\[(\d+)\]/g, (match, i) => {
const tag = event.tags[parseInt(match, 10)];
if (tag) {
const tagTarget = tag[1].replace('@', '');
if (tag[0] === 'p') {
// profile
const link = `/${Key.toNostrBech32Address(tagTarget, 'npub')}`;
return (
<a href={link}>
@<Name key={tagTarget + i} pub={tagTarget} hideBadge={true} />
</a>
);
} else if (tag[0] === 'e') {
return opts.showMentionedMessages ? (
<EventComponent key={tagTarget + i} id={tagTarget} asInlineQuote={true} />
) : (
<Link className="link" href={`/${Key.toNostrBech32Address(tagTarget, 'note')}`}>
{tag[1]}
</Link>
);
}
}
return match;
});
}
// highlight hashtags, link to /search/${encodeUriComponent(hashtag)}
s = reactStringReplace(s, hashtagRegex, (match) => {
return (
<Link className="link" href={`/search/${encodeURIComponent(match)}`}>
{match}
</Link>
);
});
// remove leading and trailing newlines
s = s.map((x) => (typeof x === 'string' ? x.replace(/^\n+|\n+$/g, '') : x));
return s;
},
copyToClipboard(text: string): boolean {
if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.

View File

@ -2,8 +2,13 @@ import { memo } from 'react';
import reactStringReplace from 'react-string-replace';
import { Event } from 'nostr-tools';
import localState from '../LocalState';
import { allEmbeds, textEmbeds } from './embed';
let settings: any = {};
localState.get('settings').on((s) => (settings = s));
const HyperText = memo(
({ children, event, textOnly }: { children: string; event?: Event; textOnly?: boolean }) => {
let processedChildren = [children.trim()];
@ -11,6 +16,7 @@ const HyperText = memo(
const embeds = textOnly ? textEmbeds : allEmbeds;
embeds.forEach((embed) => {
if (settings[embed.settingsKey || ''] === false) return;
processedChildren = reactStringReplace(processedChildren, embed.regex, (match, i) => {
return embed.component({
match,

View File

@ -9,8 +9,8 @@ import Key from '../nostr/Key';
import { DecryptedEvent } from '../views/chat/ChatMessages';
import Name from './user/Name';
import HyperText from './HyperText';
import Torrent from './Torrent';
import HyperText from "./HyperText";
type Props = {
event: DecryptedEvent;

View File

@ -0,0 +1,11 @@
import Embed from './index';
const Audio: Embed = {
regex: /(https?:\/\/\S+\.(?:mp3|wav|ogg|flac))\b/gi,
settingsKey: 'enableAudio',
component: ({ match }) => {
return <audio src={match} controls={true} loop={true} />;
},
};
export default Audio;

View File

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

View File

@ -1,21 +1,22 @@
import { useState } from 'react';
import Show from '../helpers/Show';
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 }) => {
settingsKey: 'enableImages',
component: ({ match }) => {
const [showModal, setShowModal] = useState(false);
const onClick = (e) => {
e.stopPropagation();
setShowModal(true);
};
return (
<div key={key}>
<div>
<div className="relative w-full overflow-hidden object-contain my-2">
<SafeImg
onClick={onClick}

View File

@ -2,11 +2,11 @@ import Embed from './index';
const Instagram: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:instagram\.com\/)((?:p|reel)\/[\w-]{11})(?:\S+)?/g,
component: ({ match, key }) => {
settingsKey: 'enableInstagram',
component: ({ match }) => {
return (
<iframe
className="instagram"
key={key}
width="650"
height="400"
style={{ maxWidth: '100%' }}

View File

@ -0,0 +1,21 @@
import Helpers from '../../Helpers';
import Embed from './index';
const TorrentEmbed: Embed = {
regex:
/(lightning:[\w.-]+@[\w.-]+|lightning:\w+\?amount=\d+|(?:lightning:)?(?:lnurl|lnbc)[\da-z0-9]+)/gi,
component: ({ match }) => {
if (!match.startsWith('lightning:')) {
match = `lightning:${match}`;
}
// TODO parse invoice and show amount
return (
<a href={match} onClick={(e) => Helpers.handleLightningLinkClick(e)}>
Pay with lightning
</a>
);
},
};
export default TorrentEmbed;

View File

@ -3,10 +3,10 @@ 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 }) => {
settingsKey: 'enableSoundCloud',
component: ({ match }) => {
return (
<iframe
key={key}
className="audio"
scrolling="no"
width="650"

View File

@ -0,0 +1,23 @@
import Embed from './index';
const Tidal: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:tidal\.com(?:\/browse)?\/track\/)([\d]+)?/g,
settingsKey: 'enableTidal',
component: ({ match }) => {
return (
<iframe
className="audio"
scrolling="no"
width="650"
height="200"
style={{ maxWidth: '100%' }}
src={`https://embed.tidal.com/tracks/${match}?layout=gridify`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default Tidal;

View File

@ -0,0 +1,22 @@
import Embed from './index';
const TikTok: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?tiktok\.com\/.*?video\/(\d{1,19})/g,
settingsKey: 'enableTikTok',
component: ({ match }) => {
return (
<iframe
className="tiktok"
width="605"
height="400"
style={{ maxWidth: '100%' }}
src={`https://www.tiktok.com/embed/v2/${match}`}
frameBorder="1"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default TikTok;

View File

@ -0,0 +1,13 @@
import Torrent from '../Torrent';
import Embed from './index';
const TorrentEmbed: Embed = {
regex: /(magnet:\?xt=urn:btih:.*)/gi,
settingsKey: 'enableTorrent',
component: ({ match }) => {
return <Torrent preview={true} torrentId={match} />;
},
};
export default TorrentEmbed;

View File

@ -2,7 +2,8 @@ import Embed from './index';
const Twitter: Embed = {
regex: /(?:^|\s)(?:@)?(https?:\/\/twitter.com\/\w+\/status\/\d+\S*)(?![\w/])/g,
component: ({ match, key }) => {
settingsKey: 'enableTwitter',
component: ({ match }) => {
return (
<iframe
style={{
@ -11,7 +12,6 @@ const Twitter: Embed = {
backgroundColor: 'white',
display: 'block',
}}
key={key}
scrolling="no"
height={250}
width={550}

View File

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

View File

@ -2,8 +2,9 @@ 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">
settingsKey: 'enableVideo',
component: ({ match }) => (
<div 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}

View File

@ -0,0 +1,21 @@
import Embed from './index';
const WavLake: Embed = {
regex:
/https:\/\/(?:player\.)?wavlake\.com\/(?!feed\/|artists)(track\/[.a-zA-Z0-9-]+|album\/[.a-zA-Z0-9-]+|[.a-zA-Z0-9-]+)/i,
settingsKey: 'enableWavLake',
component: ({ match }) => {
return (
<iframe
height="380"
width="100%"
style={{ maxWidth: '100%' }}
src={`https://embed.wavlake.com/${match}`}
frameBorder="0"
loading="lazy"
/>
);
},
};
export default WavLake;

View File

@ -3,10 +3,10 @@ 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 }) => {
settingsKey: 'enableYoutube',
component: ({ match }) => {
return (
<iframe
key={key}
width="650"
height="400"
src={`https://www.youtube.com/embed/${match}`}

View File

@ -0,0 +1,23 @@
import Embed from '../index';
const AppleMusic: Embed = {
regex: /(?:https?:\/\/)(?:.*?)(music\.apple\.com\/.*)/gi,
settingsKey: 'enableAppleMusic',
component: ({ match }) => {
return (
<iframe
className="applemusic"
scrolling="no"
width="650"
height="150"
style={{ maxWidth: '100%' }}
src={`https://embed.music.apple.com/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default AppleMusic;

View File

@ -0,0 +1,25 @@
import Embed from '../index';
const ApplePodcast: Embed = {
regex: /(?:https?:\/\/)(?:.*?)(music\.apple\.com\/.*)/gi,
settingsKey: 'enableAppleMusic',
component: ({ match }) => {
const cssClass = match.includes('?i=') ? 'applepodcast-small' : 'applepodcast-large';
return (
<iframe
// class="applepodcast"
className={cssClass}
scrolling="no"
width="650"
height="175"
style={{ maxWidth: '100%' }}
src={`https://embed.${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default ApplePodcast;

View File

@ -1,18 +1,31 @@
import { Event } from 'nostr-tools';
import { JSX } from 'preact';
import AppleMusic from './apple/AppleMusic';
import ApplePodcast from './apple/ApplePodcast';
import InlineMention from './nostr/InlineMention';
import Nip19 from './nostr/Nip19';
import NostrEvent from './nostr/NostrNote';
import NostrNpub from './nostr/NostrNpub';
import SpotifyAlbum from './spotify/SpotifyAlbum';
import SpotifyPlaylist from './spotify/SpotifyPlaylist';
import SpotifyPodcast from './spotify/SpotifyPodcast';
import SpotifyTrack from './spotify/SpotifyTrack';
import Twitch from './twitch/Twitch';
import TwitchChannel from './twitch/TwitchChannel';
import Audio from './Audio';
import Hashtag from './Hashtag';
import Image from './Image';
import Instagram from './Instagram';
import LightningUri from './LightningUri';
import SoundCloud from './SoundCloud';
import Tidal from './Tidal';
import TikTok from './TikTok';
import Torrent from './Torrent';
import Twitter from './Twitter';
import Url from './Url';
import Video from './Video';
import WavLake from './WavLake';
import Youtube from './YouTube';
export type EmbedProps = {
@ -25,9 +38,11 @@ export type EmbedProps = {
type Embed = {
regex: RegExp;
component: (props: EmbedProps) => JSX.Element;
settingsKey?: string;
};
export const allEmbeds = [
Audio,
Image,
Video,
Youtube,
@ -35,6 +50,18 @@ export const allEmbeds = [
Twitter,
SoundCloud,
SpotifyTrack,
SpotifyAlbum,
SpotifyPodcast,
SpotifyPlaylist,
AppleMusic,
ApplePodcast,
Tidal,
TikTok,
Twitch,
TwitchChannel,
WavLake,
Torrent,
LightningUri,
NostrNpub,
NostrEvent,
Nip19,

View File

@ -5,14 +5,13 @@ 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 }) => {
component: ({ match, index, event }) => {
if (!event?.tags) {
console.log('no tags', event);
return <>{fail(match)}</>;
@ -25,7 +24,7 @@ const InlineMention: Embed = {
const [type, id] = tag;
if (type === 'p') {
return (
<Link key={key} href={`/${nip19.npubEncode(id)}`} className="link">
<Link href={`/${nip19.npubEncode(id)}`} className="link">
<Name pub={id} hideBadge={true} />
</Link>
);

View File

@ -3,21 +3,20 @@ 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 }) => {
component: ({ match }) => {
try {
const { type, data } = nip19.decode(match);
if (type === 'nprofile') {
return (
<>
{' '}
<Link key={key} className="text-iris-blue hover:underline" href={`/${data.pubkey}`}>
<Link className="text-iris-blue hover:underline" href={`/${data.pubkey}`}>
<Name pub={data.pubkey} />
</Link>
</>
@ -25,7 +24,7 @@ const NostrUser: Embed = {
} else if (type === 'nevent') {
// same as note
return (
<div key={key} className="rounded-lg border border-gray-500 my-2">
<div className="rounded-lg border border-gray-500 my-2">
<EventComponent id={data.id} asInlineQuote={true} />
</div>
);
@ -33,7 +32,7 @@ const NostrUser: Embed = {
} catch (e) {
console.log(e);
}
return <span key={key}>{match}</span>;
return <span>{match}</span>;
},
};

View File

@ -1,6 +1,5 @@
import Key from '../../../nostr/Key';
import EventComponent from '../../events/EventComponent';
import Embed from '../index';
const eventRegex =
@ -8,9 +7,9 @@ const eventRegex =
const NostrUser: Embed = {
regex: eventRegex,
component: ({ match, key }) => {
component: ({ match }) => {
const hex = Key.toNostrHexAddress(match.replace('@', ''))!;
return <EventComponent key={key} id={hex} asInlineQuote={true} />;
return <EventComponent id={hex} asInlineQuote={true} />;
},
};

View File

@ -1,7 +1,6 @@
import { Link } from 'preact-router';
import Name from '../../user/Name';
import Embed from '../index';
const pubKeyRegex =
@ -9,10 +8,10 @@ const pubKeyRegex =
const NostrNpub: Embed = {
regex: pubKeyRegex,
component: ({ match, key }) => {
component: ({ match }) => {
const pub = match.replace('@', '');
return (
<Link key={key} href={`/${pub}`} className="link mr-1">
<Link href={`/${pub}`} className="link mr-1">
<Name pub={pub} hideBadge={true} />
</Link>
);

View File

@ -0,0 +1,22 @@
import Embed from '../index';
const SpotifyAlbum: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/album\/)([\w-]+)(?:\S+)?/g,
settingsKey: 'enableSpotify',
component: ({ match }) => {
return (
<iframe
className="audio"
scrolling="no"
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://open.spotify.com/embed/album/${match}`}
frameBorder="0"
allow="encrypted-media"
/>
);
},
};
export default SpotifyAlbum;

View File

@ -0,0 +1,23 @@
import Embed from '../index';
const SpotifyPlaylist: Embed = {
regex: /(?:https?:\/\/)(?:.*?)(music\.apple\.com\/.*)/gi,
settingsKey: 'enableSpotify',
component: ({ match }) => {
return (
<iframe
className="applemusic"
scrolling="no"
width="650"
height="150"
style={{ maxWidth: '100%' }}
src={`https://embed.music.apple.com/${match}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default SpotifyPlaylist;

View File

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

View File

@ -2,12 +2,12 @@ import Embed from '../index';
const SpotifyTrack: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:open\.spotify\.com\/track\/)([\w-]+)(?:\S+)?/g,
component: ({ match, key }) => {
settingsKey: 'enableSpotify',
component: ({ match }) => {
return (
<iframe
className="audio"
scrolling="no"
key={key}
width="650"
height="200"
style={{ maxWidth: '100%' }}

View File

@ -0,0 +1,23 @@
import Embed from '../index';
const Twitch: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:twitch\.tv\/videos\/)([\d]+)?/g,
settingsKey: 'enableTwitch',
component: ({ match }) => {
return (
<iframe
className="video"
scrolling="no"
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://player.twitch.tv/?video=${match}&parent=${window.location.hostname}&autoplay=false`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default Twitch;

View File

@ -0,0 +1,23 @@
import Embed from '../index';
const Twitch: Embed = {
regex: /(?:https?:\/\/)?(?:www\.)?(?:twitch\.tv\/)([\w-]+)?/g,
settingsKey: 'enableTwitch',
component: ({ match }) => {
return (
<iframe
className="video"
scrolling="no"
width="650"
height="400"
style={{ maxWidth: '100%' }}
src={`https://player.twitch.tv/?channel=${match}&parent=${window.location.hostname}&autoplay=false`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
},
};
export default Twitch;

View File

@ -11,13 +11,13 @@ import { ID } from '../../nostr/UserIds';
import { translate as t } from '../../translations/Translation.mjs';
import Follow from '../buttons/Follow';
import Show from '../helpers/Show';
import HyperText from '../HyperText';
import Avatar from './Avatar';
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;