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<{ } >
- avatar + + }> + +
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 = () => {
- +
- +