mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Add zoomable images
This commit is contained in:
parent
32f41a9b20
commit
ffe205c658
10
package-lock.json
generated
10
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "primal-web-app",
|
"name": "primal-web-app",
|
||||||
"version": "0.77.3",
|
"version": "0.77.45",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "primal-web-app",
|
"name": "primal-web-app",
|
||||||
"version": "0.77.3",
|
"version": "0.77.45",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cookbook/solid-intl": "^0.1.2",
|
"@cookbook/solid-intl": "^0.1.2",
|
||||||
@ -17,6 +17,7 @@
|
|||||||
"@thisbeyond/solid-select": "^0.13.0",
|
"@thisbeyond/solid-select": "^0.13.0",
|
||||||
"@types/dompurify": "^2.4.0",
|
"@types/dompurify": "^2.4.0",
|
||||||
"dompurify": "^3.0.0",
|
"dompurify": "^3.0.0",
|
||||||
|
"medium-zoom": "^1.0.8",
|
||||||
"nostr-tools": "^1.4.1",
|
"nostr-tools": "^1.4.1",
|
||||||
"sass": "^1.58.0",
|
"sass": "^1.58.0",
|
||||||
"solid-js": "^1.6.6"
|
"solid-js": "^1.6.6"
|
||||||
@ -1589,6 +1590,11 @@
|
|||||||
"remove-accents": "0.4.2"
|
"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": {
|
"node_modules/merge-anything": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.4.tgz",
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"@thisbeyond/solid-select": "^0.13.0",
|
"@thisbeyond/solid-select": "^0.13.0",
|
||||||
"@types/dompurify": "^2.4.0",
|
"@types/dompurify": "^2.4.0",
|
||||||
"dompurify": "^3.0.0",
|
"dompurify": "^3.0.0",
|
||||||
|
"medium-zoom": "^1.0.8",
|
||||||
"nostr-tools": "^1.4.1",
|
"nostr-tools": "^1.4.1",
|
||||||
"sass": "^1.58.0",
|
"sass": "^1.58.0",
|
||||||
"solid-js": "^1.6.6"
|
"solid-js": "^1.6.6"
|
||||||
|
@ -3,6 +3,7 @@ import defaultAvatar from '../../assets/icons/default_avatar.svg';
|
|||||||
import { useMediaContext } from '../../contexts/MediaContext';
|
import { useMediaContext } from '../../contexts/MediaContext';
|
||||||
import { hookForDev } from '../../lib/devTools';
|
import { hookForDev } from '../../lib/devTools';
|
||||||
import { MediaSize, PrimalUser } from '../../types/primal';
|
import { MediaSize, PrimalUser } from '../../types/primal';
|
||||||
|
import NoteImage from '../NoteImage/NoteImage';
|
||||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||||
|
|
||||||
import styles from './Avatar.module.scss';
|
import styles from './Avatar.module.scss';
|
||||||
@ -14,6 +15,7 @@ const Avatar: Component<{
|
|||||||
highlightBorder?: boolean,
|
highlightBorder?: boolean,
|
||||||
id?: string,
|
id?: string,
|
||||||
showCheck?: boolean,
|
showCheck?: boolean,
|
||||||
|
zoomable?: boolean,
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
|
||||||
const media = useMediaContext();
|
const media = useMediaContext();
|
||||||
@ -119,7 +121,11 @@ const Avatar: Component<{
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class={`${styles.missingBack} ${notCachedFlag()}`}>
|
<div class={`${styles.missingBack} ${notCachedFlag()}`}>
|
||||||
<img src={imageSrc()} alt="avatar" onerror={imgError}/>
|
<Show when={props.zoomable} fallback={
|
||||||
|
<img src={imageSrc()} alt="avatar" onerror={imgError}/>
|
||||||
|
}>
|
||||||
|
<NoteImage src={imageSrc()} onError={imgError} />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.user && props.showCheck}>
|
<Show when={props.user && props.showCheck}>
|
||||||
|
8
src/components/NoteImage/NoteImage.module.scss
Normal file
8
src/components/NoteImage/NoteImage.module.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.noteImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 22;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
58
src/components/NoteImage/NoteImage.tsx
Normal file
58
src/components/NoteImage/NoteImage.tsx
Normal file
@ -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<HTMLImageElement, Event>,
|
||||||
|
}> = (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 <img id={imgId} src={props.src} class={klass()} onerror={props.onError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoteImage;
|
@ -1,6 +1,6 @@
|
|||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { hexToNpub } from '../../lib/keys';
|
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 { truncateNpub, userName } from '../../stores/profile';
|
||||||
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
|
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
|
||||||
import {
|
import {
|
||||||
@ -287,7 +287,6 @@ const ParsedNote: Component<{
|
|||||||
setDisplayedContent(() => newContent);
|
setDisplayedContent(() => newContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={props.id} innerHTML={displayedContent()}>
|
<div id={props.id} innerHTML={displayedContent()}>
|
||||||
</div>
|
</div>
|
||||||
|
@ -396,6 +396,18 @@ body {
|
|||||||
overflow: hidden;
|
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
|
// Scrollbars
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
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";
|
||||||
|
import NoteImage from "../components/NoteImage/NoteImage";
|
||||||
import { Kind } from "../constants";
|
import { Kind } from "../constants";
|
||||||
import { sendMessage, subscribeTo } from "../sockets";
|
import { sendMessage, subscribeTo } from "../sockets";
|
||||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
|
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 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 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 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 = (
|
export const urlify = (
|
||||||
text: string,
|
text: string,
|
||||||
@ -66,43 +84,38 @@ export const urlify = (
|
|||||||
skipEmbed = false,
|
skipEmbed = false,
|
||||||
skipLinkPreview = 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) {
|
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(url)) {
|
||||||
|
|
||||||
if (isImage) {
|
|
||||||
const dev = localStorage.getItem('devMode') === 'true';
|
const dev = localStorage.getItem('devMode') === 'true';
|
||||||
let imgUrl = getMediaUrl && getMediaUrl(url);
|
let imgUrl = getMediaUrl && getMediaUrl(url);
|
||||||
|
|
||||||
if (!imgUrl) {
|
if (!imgUrl) {
|
||||||
return `<img src="${getMediaUrlDefault(url)}" class="postImage${dev ? ' redBorder' : ''}"/>`;
|
// @ts-ignore
|
||||||
|
return (<div><NoteImage src={getMediaUrlDefault(url)} isDev={dev} /></div>).outerHTML;
|
||||||
|
// return `<img src="${getMediaUrlDefault(url)}" class="postImage${dev ? ' redBorder' : ''}"/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<img src="${imgUrl}" class="postImage"/>`;
|
// @ts-ignore
|
||||||
|
return (<div><NoteImage src={imgUrl} isDev={dev} /></div>).outerHTML;
|
||||||
|
// return `<img src="${imgUrl}" class="postImage"/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMp4Video = url.includes('.mp4') || url.includes('.mov');
|
if (isMp4Video(url)) {
|
||||||
|
|
||||||
if (isMp4Video) {
|
|
||||||
return `<video class="w-max" controls><source src="${url}" type="video/mp4"></video>`;
|
return `<video class="w-max" controls><source src="${url}" type="video/mp4"></video>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOggVideo = url.includes('.ogg');
|
if (isOggVideo(url)) {
|
||||||
|
|
||||||
if (isOggVideo) {
|
|
||||||
return `<video class="w-max" controls><source src="${url}" type="video/ogg"></video>`;
|
return `<video class="w-max" controls><source src="${url}" type="video/ogg"></video>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWebmVideo = url.includes('.webm');
|
if (isWebmVideo(url)) {
|
||||||
|
|
||||||
if (isWebmVideo) {
|
|
||||||
return `<video class="w-max" controls><source src="${url}" type="video/webm"></video>`;
|
return `<video class="w-max" controls><source src="${url}" type="video/webm"></video>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (youtubeRegex.test(url)) {
|
if (isYouTube(url)) {
|
||||||
const youtubeId = youtubeRegex.test(url) && RegExp.$1;
|
const youtubeId = youtubeRegex.test(url) && RegExp.$1;
|
||||||
|
|
||||||
return `<iframe
|
return `<iframe
|
||||||
@ -116,20 +129,20 @@ export const urlify = (
|
|||||||
></iframe>`;
|
></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spotifyRegex.test(url)) {
|
if (isSpotify(url)) {
|
||||||
const convertedUrl = url.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
const convertedUrl = url.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
||||||
|
|
||||||
return `<iframe style="borderRadius: 12" src="${convertedUrl}" width="100%" height="352" frameBorder="0" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>`;
|
return `<iframe style="borderRadius: 12" src="${convertedUrl}" width="100%" height="352" frameBorder="0" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (twitchRegex.test(url)) {
|
if (isTwitch(url)) {
|
||||||
const channel = url.split("/").slice(-1);
|
const channel = url.split("/").slice(-1);
|
||||||
|
|
||||||
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
||||||
return `<iframe src="https://player.twitch.tv/${args}" className="w-max" allowFullScreen></iframe>`;
|
return `<iframe src="https://player.twitch.tv/${args}" className="w-max" allowFullScreen></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mixCloudRegex.test(url)) {
|
if (isMixCloud(url)) {
|
||||||
const feedPath = (mixCloudRegex.test(url) && RegExp.$1) + "%2F" + (mixCloudRegex.test(url) && RegExp.$2);
|
const feedPath = (mixCloudRegex.test(url) && RegExp.$1) + "%2F" + (mixCloudRegex.test(url) && RegExp.$2);
|
||||||
|
|
||||||
// const lightTheme = useLogin().preferences.theme === "light";
|
// const lightTheme = useLogin().preferences.theme === "light";
|
||||||
@ -145,7 +158,7 @@ export const urlify = (
|
|||||||
></iframe>`;
|
></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (soundCloudRegex.test(url)) {
|
if (isSoundCloud(url)) {
|
||||||
return `<iframe
|
return `<iframe
|
||||||
width="100%"
|
width="100%"
|
||||||
height="166"
|
height="166"
|
||||||
@ -154,7 +167,7 @@ export const urlify = (
|
|||||||
src="https://w.soundcloud.com/player/?url=${url}"></iframe>`;
|
src="https://w.soundcloud.com/player/?url=${url}"></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appleMusicRegex.test(url)) {
|
if (isAppleMusic(url)) {
|
||||||
const convertedUrl = url.replace("music.apple.com", "embed.music.apple.com");
|
const convertedUrl = url.replace("music.apple.com", "embed.music.apple.com");
|
||||||
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
||||||
|
|
||||||
@ -169,7 +182,7 @@ export const urlify = (
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nostrNestsRegex.test(url)) {
|
if (isNostrNests(url)) {
|
||||||
return `
|
return `
|
||||||
<iframe
|
<iframe
|
||||||
src="${url}"
|
src="${url}"
|
||||||
@ -181,7 +194,7 @@ export const urlify = (
|
|||||||
></iframe>`;
|
></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wavlakeRegex.test(url)) {
|
if (isWavelake(url)) {
|
||||||
const convertedUrl = url.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
const convertedUrl = url.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
@ -479,11 +479,11 @@ const Profile: Component = () => {
|
|||||||
<div class={styles.userImage}>
|
<div class={styles.userImage}>
|
||||||
<div class={styles.avatar}>
|
<div class={styles.avatar}>
|
||||||
<div class={styles.desktopAvatar}>
|
<div class={styles.desktopAvatar}>
|
||||||
<Avatar user={profile?.userProfile} size="xxl" />
|
<Avatar user={profile?.userProfile} size="xxl" zoomable={true} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={styles.phoneAvatar}>
|
<div class={styles.phoneAvatar}>
|
||||||
<Avatar user={profile?.userProfile} size="lg" />
|
<Avatar user={profile?.userProfile} size="lg" zoomable={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user