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",
|
||||
"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",
|
||||
|
@ -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"
|
||||
|
@ -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<{
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</Show>
|
||||
<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 { 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 (
|
||||
<div id={props.id} innerHTML={displayedContent()}>
|
||||
</div>
|
||||
|
@ -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 */
|
||||
|
@ -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 `<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) {
|
||||
if (isMp4Video(url)) {
|
||||
return `<video class="w-max" controls><source src="${url}" type="video/mp4"></video>`;
|
||||
}
|
||||
|
||||
const isOggVideo = url.includes('.ogg');
|
||||
|
||||
if (isOggVideo) {
|
||||
if (isOggVideo(url)) {
|
||||
return `<video class="w-max" controls><source src="${url}" type="video/ogg"></video>`;
|
||||
}
|
||||
|
||||
const isWebmVideo = url.includes('.webm');
|
||||
|
||||
if (isWebmVideo) {
|
||||
if (isWebmVideo(url)) {
|
||||
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;
|
||||
|
||||
return `<iframe
|
||||
@ -116,20 +129,20 @@ export const urlify = (
|
||||
></iframe>`;
|
||||
}
|
||||
|
||||
if (spotifyRegex.test(url)) {
|
||||
if (isSpotify(url)) {
|
||||
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>`;
|
||||
}
|
||||
|
||||
if (twitchRegex.test(url)) {
|
||||
if (isTwitch(url)) {
|
||||
const channel = url.split("/").slice(-1);
|
||||
|
||||
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
||||
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 lightTheme = useLogin().preferences.theme === "light";
|
||||
@ -145,7 +158,7 @@ export const urlify = (
|
||||
></iframe>`;
|
||||
}
|
||||
|
||||
if (soundCloudRegex.test(url)) {
|
||||
if (isSoundCloud(url)) {
|
||||
return `<iframe
|
||||
width="100%"
|
||||
height="166"
|
||||
@ -154,7 +167,7 @@ export const urlify = (
|
||||
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 isSongLink = /\?i=\d+$/.test(convertedUrl);
|
||||
|
||||
@ -169,7 +182,7 @@ export const urlify = (
|
||||
`;
|
||||
}
|
||||
|
||||
if (nostrNestsRegex.test(url)) {
|
||||
if (isNostrNests(url)) {
|
||||
return `
|
||||
<iframe
|
||||
src="${url}"
|
||||
@ -181,7 +194,7 @@ export const urlify = (
|
||||
></iframe>`;
|
||||
}
|
||||
|
||||
if (wavlakeRegex.test(url)) {
|
||||
if (isWavelake(url)) {
|
||||
const convertedUrl = url.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||
|
||||
return `
|
||||
|
@ -479,11 +479,11 @@ const Profile: Component = () => {
|
||||
<div class={styles.userImage}>
|
||||
<div class={styles.avatar}>
|
||||
<div class={styles.desktopAvatar}>
|
||||
<Avatar user={profile?.userProfile} size="xxl" />
|
||||
<Avatar user={profile?.userProfile} size="xxl" zoomable={true} />
|
||||
</div>
|
||||
|
||||
<div class={styles.phoneAvatar}>
|
||||
<Avatar user={profile?.userProfile} size="lg" />
|
||||
<Avatar user={profile?.userProfile} size="lg" zoomable={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user