diff --git a/package-lock.json b/package-lock.json
index 4369757..ac0a428 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "primal-web-app",
- "version": "0.77.3",
+ "version": "0.77.45",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "primal-web-app",
- "version": "0.77.3",
+ "version": "0.77.45",
"license": "MIT",
"dependencies": {
"@cookbook/solid-intl": "^0.1.2",
@@ -17,6 +17,7 @@
"@thisbeyond/solid-select": "^0.13.0",
"@types/dompurify": "^2.4.0",
"dompurify": "^3.0.0",
+ "medium-zoom": "^1.0.8",
"nostr-tools": "^1.4.1",
"sass": "^1.58.0",
"solid-js": "^1.6.6"
@@ -1589,6 +1590,11 @@
"remove-accents": "0.4.2"
}
},
+ "node_modules/medium-zoom": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.0.8.tgz",
+ "integrity": "sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA=="
+ },
"node_modules/merge-anything": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.4.tgz",
diff --git a/package.json b/package.json
index 6d44dd8..3d29950 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@thisbeyond/solid-select": "^0.13.0",
"@types/dompurify": "^2.4.0",
"dompurify": "^3.0.0",
+ "medium-zoom": "^1.0.8",
"nostr-tools": "^1.4.1",
"sass": "^1.58.0",
"solid-js": "^1.6.6"
diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx
index 5f5cd3c..e9a6ef7 100644
--- a/src/components/Avatar/Avatar.tsx
+++ b/src/components/Avatar/Avatar.tsx
@@ -3,6 +3,7 @@ import defaultAvatar from '../../assets/icons/default_avatar.svg';
import { useMediaContext } from '../../contexts/MediaContext';
import { hookForDev } from '../../lib/devTools';
import { MediaSize, PrimalUser } from '../../types/primal';
+import NoteImage from '../NoteImage/NoteImage';
import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './Avatar.module.scss';
@@ -14,6 +15,7 @@ const Avatar: Component<{
highlightBorder?: boolean,
id?: string,
showCheck?: boolean,
+ zoomable?: boolean,
}> = (props) => {
const media = useMediaContext();
@@ -119,7 +121,11 @@ const Avatar: Component<{
}
>
-
+
+ }>
+
+
diff --git a/src/components/NoteImage/NoteImage.module.scss b/src/components/NoteImage/NoteImage.module.scss
new file mode 100644
index 0000000..bc94c5b
--- /dev/null
+++ b/src/components/NoteImage/NoteImage.module.scss
@@ -0,0 +1,8 @@
+.noteImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 8px;
+ z-index: 22;
+ cursor: pointer;
+}
diff --git a/src/components/NoteImage/NoteImage.tsx b/src/components/NoteImage/NoteImage.tsx
new file mode 100644
index 0000000..da45f78
--- /dev/null
+++ b/src/components/NoteImage/NoteImage.tsx
@@ -0,0 +1,58 @@
+import { Component, 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";
+
+const NoteImage: Component<{
+ src?: string,
+ isDev?: boolean,
+ onError?: JSX.EventHandlerUnion,
+}> = (props) => {
+ const imgId = generatePrivateKey();
+
+ const imgRef = () => {
+ return document.getElementById(imgId)
+ };
+
+ let zoomRef: Zoom | undefined;
+
+ const klass = () => `${styles.noteImage} ${props.isDev ? 'redBorder' : ''}`;
+
+ const doZoom = (e: MouseEvent) => {
+ if (!e.target || (e.target as HTMLImageElement).id !== imgId) {
+ return;
+ }
+
+ zoomRef?.open();
+ };
+
+ const getZoom = () => {
+ const iRef = imgRef();
+ if (zoomRef || !iRef) {
+ return zoomRef;
+ }
+
+ zoomRef = mediumZoom(iRef, {
+ background: "var(--background-site)",
+ });
+
+ zoomRef.attach(iRef);
+ }
+
+ onMount(() => {
+ getZoom();
+ document.addEventListener('click', doZoom)
+ });
+
+ onCleanup(() => {
+ const iRef = imgRef();
+ iRef && zoomRef && zoomRef.detach(iRef);
+ document.removeEventListener('click', doZoom)
+ });
+
+ return ;
+}
+
+export default NoteImage;
diff --git a/src/components/ParsedNote/ParsedNote.tsx b/src/components/ParsedNote/ParsedNote.tsx
index af5ffeb..de8e519 100644
--- a/src/components/ParsedNote/ParsedNote.tsx
+++ b/src/components/ParsedNote/ParsedNote.tsx
@@ -1,6 +1,6 @@
import { A } from '@solidjs/router';
import { hexToNpub } from '../../lib/keys';
-import { linkPreviews, parseNote1, parseNote3 } from '../../lib/notes';
+import { linkPreviews, parseNote1 } from '../../lib/notes';
import { truncateNpub, userName } from '../../stores/profile';
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
import {
@@ -287,7 +287,6 @@ const ParsedNote: Component<{
setDisplayedContent(() => newContent);
});
-
return (
diff --git a/src/index.scss b/src/index.scss
index c9ce5f9..886ceb0 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -396,6 +396,18 @@ body {
overflow: hidden;
}
+.medium-zoom-image {
+ cursor: pointer !important;
+}
+.medium-zoom-overlay {
+ cursor: pointer !important;
+ z-index: var(--z-index-header);
+}
+.medium-zoom-image-opened {
+ z-index: var(--z-index-floater);
+
+}
+
// Scrollbars
/* width */
diff --git a/src/lib/notes.tsx b/src/lib/notes.tsx
index 9ed7c83..fa8b8ec 100644
--- a/src/lib/notes.tsx
+++ b/src/lib/notes.tsx
@@ -2,6 +2,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 { sendMessage, subscribeTo } from "../sockets";
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
@@ -58,6 +59,23 @@ 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 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 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));
+export const isOggVideo = (url: string) => ['.ogg'].some(x => url.includes(x));
+export const isWebmVideo = (url: string) => ['.webm'].some(x => url.includes(x));
+
+export const isYouTube = (url: string) => youtubeRegex.test(url);
+export const isSpotify = (url: string) => spotifyRegex.test(url);
+export const isTwitch = (url: string) => twitchRegex.test(url);
+export const isMixCloud = (url: string) => mixCloudRegex.test(url);
+export const isSoundCloud = (url: string) => soundCloudRegex.test(url);
+export const isAppleMusic = (url: string) => appleMusicRegex.test(url);
+export const isNostrNests = (url: string) => nostrNestsRegex.test(url);
+export const isWavelake = (url: string) => wavlakeRegex.test(url);
+
export const urlify = (
text: string,
@@ -66,43 +84,38 @@ export const urlify = (
skipEmbed = false,
skipLinkPreview = false,
) => {
- 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;
- return text.replace(urlRegex, (url) => {
+ return text.replace(urlRegex, (url: string) => {
if (!skipEmbed) {
- const isImage = url.includes('.jpg')|| url.includes('.jpeg')|| url.includes('.webp') || url.includes('.png') || url.includes('.gif') || url.includes('format=png');
-
- if (isImage) {
+ if (isImage(url)) {
const dev = localStorage.getItem('devMode') === 'true';
let imgUrl = getMediaUrl && getMediaUrl(url);
if (!imgUrl) {
- return ``;
+ // @ts-ignore
+ return (
).outerHTML;
+ // return ``;
}
- return ``;
+ // @ts-ignore
+ return (
).outerHTML;
+ // return ``;
}
- const isMp4Video = url.includes('.mp4') || url.includes('.mov');
-
- if (isMp4Video) {
+ if (isMp4Video(url)) {
return ``;
}
- const isOggVideo = url.includes('.ogg');
-
- if (isOggVideo) {
+ if (isOggVideo(url)) {
return ``;
}
- const isWebmVideo = url.includes('.webm');
-
- if (isWebmVideo) {
+ if (isWebmVideo(url)) {
return ``;
}
- if (youtubeRegex.test(url)) {
+ if (isYouTube(url)) {
const youtubeId = youtubeRegex.test(url) && RegExp.$1;
return ``;
}
- if (spotifyRegex.test(url)) {
+ if (isSpotify(url)) {
const convertedUrl = url.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
return ``;
}
- if (twitchRegex.test(url)) {
+ if (isTwitch(url)) {
const channel = url.split("/").slice(-1);
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
return ``;
}
- if (mixCloudRegex.test(url)) {
+ if (isMixCloud(url)) {
const feedPath = (mixCloudRegex.test(url) && RegExp.$1) + "%2F" + (mixCloudRegex.test(url) && RegExp.$2);
// const lightTheme = useLogin().preferences.theme === "light";
@@ -145,7 +158,7 @@ export const urlify = (
>`;
}
- if (soundCloudRegex.test(url)) {
+ if (isSoundCloud(url)) {
return ``;
}
- if (appleMusicRegex.test(url)) {
+ if (isAppleMusic(url)) {
const convertedUrl = url.replace("music.apple.com", "embed.music.apple.com");
const isSongLink = /\?i=\d+$/.test(convertedUrl);
@@ -169,7 +182,7 @@ export const urlify = (
`;
}
- if (nostrNestsRegex.test(url)) {
+ if (isNostrNests(url)) {
return `
`;
}
- if (wavlakeRegex.test(url)) {
+ if (isWavelake(url)) {
const convertedUrl = url.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
return `
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx
index 6df8d30..6d9d85e 100644
--- a/src/pages/Profile.tsx
+++ b/src/pages/Profile.tsx
@@ -479,11 +479,11 @@ const Profile: Component = () => {