feat: Added details dialog
This commit is contained in:
parent
fbe06a623d
commit
13a61bce9c
BIN
public/notfound.png
Normal file
BIN
public/notfound.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
15
src/App.tsx
15
src/App.tsx
@ -1,28 +1,15 @@
|
|||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
|
||||||
import SlideShow from './components/SlideShow';
|
import SlideShow from './components/SlideShow';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Disclaimer from './components/Disclaimer';
|
import Disclaimer from './components/Disclaimer';
|
||||||
import useDisclaimerState from './utils/useDisclaimerState';
|
import useDisclaimerState from './utils/useDisclaimerState';
|
||||||
import { defaultHashTags } from './components/env';
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { disclaimerAccepted, setDisclaimerAccepted } = useDisclaimerState();
|
const { disclaimerAccepted, setDisclaimerAccepted } = useDisclaimerState();
|
||||||
|
|
||||||
const { tags, npub } = useParams();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const nsfw = searchParams.get('nsfw') === 'true';
|
|
||||||
|
|
||||||
console.log(`tags = ${tags}, npub = ${npub}, nsfw = ${nsfw}`);
|
|
||||||
|
|
||||||
let useTags = tags?.split(',') || [];
|
|
||||||
if (npub == undefined && (useTags == undefined || useTags.length == 0)) {
|
|
||||||
useTags = defaultHashTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{disclaimerAccepted ? (
|
{disclaimerAccepted ? (
|
||||||
<SlideShow tags={useTags} npubs={npub ? [npub] : []} showNsfw={nsfw} />
|
<SlideShow />
|
||||||
) : (
|
) : (
|
||||||
<Disclaimer disclaimerAccepted={disclaimerAccepted} setDisclaimerAccepted={setDisclaimerAccepted} />
|
<Disclaimer disclaimerAccepted={disclaimerAccepted} setDisclaimerAccepted={setDisclaimerAccepted} />
|
||||||
)}
|
)}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import './Disclaimer.css';
|
import './Disclaimer.css';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
|
import useNav from '../utils/useNav';
|
||||||
|
|
||||||
const AdultContentInfo = () => {
|
const AdultContentInfo = () => {
|
||||||
const navigate = useNavigate();
|
const { nav, currentSettings } = useNav();
|
||||||
|
|
||||||
const proceed = (e: MouseEvent<HTMLButtonElement>) => {
|
const proceed = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nsfwPostfix = '?nsfw=true';
|
nav({ ...currentSettings, showNsfw: true });
|
||||||
navigate(`${window.location.pathname}${nsfwPostfix}`);
|
|
||||||
};
|
};
|
||||||
const goBack = (e: MouseEvent<HTMLButtonElement>) => {
|
const goBack = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(`/`);
|
nav({ npubs: [], tags: [], showNsfw: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,29 +1,37 @@
|
|||||||
import './SlideShow.css';
|
import './SlideShow.css';
|
||||||
import useImageLoaded from '../utils/useImageLoaded';
|
import useImageLoaded from '../utils/useImageLoaded';
|
||||||
|
import { createImgProxyUrl } from './nostrImageDownload';
|
||||||
|
import useNav from '../utils/useNav';
|
||||||
|
|
||||||
type AvatarImageProps = {
|
type AvatarImageProps = {
|
||||||
src?: string;
|
src?: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
npub?: string;
|
npub?: string;
|
||||||
|
setShowGrid?: (showGrid: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthorProfile = ({ src, author, npub }: AvatarImageProps) => {
|
const AuthorProfile = ({ src, author, npub, setShowGrid }: AvatarImageProps) => {
|
||||||
const avatarLoaded = useImageLoaded(src);
|
const avatarLoaded = useImageLoaded(src);
|
||||||
|
const { nav, currentSettings } = useNav();
|
||||||
return (
|
return (
|
||||||
<div className="author-info">
|
<div
|
||||||
<a href={`https://nostrapp.link/#${npub}`} target="_blank">
|
className="author-info"
|
||||||
<div>
|
onClick={() => {
|
||||||
{avatarLoaded && (
|
setShowGrid && setShowGrid(true);
|
||||||
<div
|
npub && nav({ ...currentSettings, tags: [], npubs: [npub] });
|
||||||
className="author-image"
|
}}
|
||||||
style={{
|
>
|
||||||
backgroundImage: `url(${src})`,
|
<div>
|
||||||
}}
|
{avatarLoaded && (
|
||||||
></div>
|
<div
|
||||||
)}
|
className="author-image"
|
||||||
{author}
|
style={{
|
||||||
</div>
|
backgroundImage: src ? `url(${createImgProxyUrl(src, 80, 80)})` : 'none',
|
||||||
</a>
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
{author}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
37
src/components/GridView/Author.tsx
Normal file
37
src/components/GridView/Author.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import useNav from '../../utils/useNav';
|
||||||
|
import { createImgProxyUrl } from '../nostrImageDownload';
|
||||||
|
import './DetailsView.css';
|
||||||
|
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||||
|
|
||||||
|
type DetailsAuthorProps = {
|
||||||
|
profile?: NDKUserProfile;
|
||||||
|
npub?: string;
|
||||||
|
setActiveImageIdx: (idx: number | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailsAuthor = ({ profile, npub, setActiveImageIdx }: DetailsAuthorProps) => {
|
||||||
|
const { nav, currentSettings } = useNav();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="details-author"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveImageIdx(undefined);
|
||||||
|
npub && nav({ ...currentSettings, tags: [], npubs: [npub] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="author-image"
|
||||||
|
style={{
|
||||||
|
backgroundImage: profile?.image ? `url(${createImgProxyUrl(profile?.image, 80, 80)})` : 'none',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{profile?.displayName || profile?.name}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailsAuthor;
|
96
src/components/GridView/DetailsView.css
Normal file
96
src/components/GridView/DetailsView.css
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
.details {
|
||||||
|
/*
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
z-index: 500;
|
||||||
|
padding: 2em;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
width: 90%;
|
||||||
|
height: 85%;
|
||||||
|
*/
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 500;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 2em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-contents {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max(60vw, 40vw) auto;
|
||||||
|
gap: 24px;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-contents .tag {
|
||||||
|
display: inline;
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
margin-right: 0.2em;
|
||||||
|
border-radius: 24px;
|
||||||
|
background-color: #444;
|
||||||
|
color: white;
|
||||||
|
line-height: 2.2em;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.details-contents .tag:hover {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-contents .detail-image {
|
||||||
|
object-fit: contain;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 30em;
|
||||||
|
width: 25em;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-author {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.details {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.details-contents {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
.detail-description {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
57
src/components/GridView/DetailsView.tsx
Normal file
57
src/components/GridView/DetailsView.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { NostrImage } from '../nostrImageDownload';
|
||||||
|
import './DetailsView.css';
|
||||||
|
import { useNDK } from '@nostr-dev-kit/ndk-react';
|
||||||
|
import DetailsAuthor from './Author';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import useNav from '../../utils/useNav';
|
||||||
|
|
||||||
|
type DetailsViewProps = {
|
||||||
|
images: NostrImage[];
|
||||||
|
activeImageIdx: number | undefined;
|
||||||
|
setActiveImageIdx: (idx: number | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailsView = ({ images, activeImageIdx, setActiveImageIdx }: DetailsViewProps) => {
|
||||||
|
const { getProfile } = useNDK();
|
||||||
|
const currentImage = useMemo(
|
||||||
|
() => (activeImageIdx !== undefined ? images[activeImageIdx] : undefined),
|
||||||
|
[images, activeImageIdx]
|
||||||
|
);
|
||||||
|
const activeProfile = currentImage?.author !== undefined ? getProfile(currentImage?.author) : undefined;
|
||||||
|
const { nav, currentSettings } = useNav();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="details">
|
||||||
|
<div className="details-contents">
|
||||||
|
<img className="detail-image" src={currentImage?.url}></img>
|
||||||
|
<div className="detail-description">
|
||||||
|
<DetailsAuthor
|
||||||
|
profile={activeProfile}
|
||||||
|
npub={currentImage?.author}
|
||||||
|
setActiveImageIdx={setActiveImageIdx}
|
||||||
|
></DetailsAuthor>
|
||||||
|
|
||||||
|
<div>{currentImage?.content}</div>
|
||||||
|
<div>
|
||||||
|
{uniq(currentImage?.tags).map(t => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="tag"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveImageIdx(undefined);
|
||||||
|
nav({ ...currentSettings, tags: [t], npubs: [] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</span>{' '}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailsView;
|
27
src/components/GridView/GridImage.tsx
Normal file
27
src/components/GridView/GridImage.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { SyntheticEvent, useState } from 'react';
|
||||||
|
import { NostrImage, createImgProxyUrl } from '../nostrImageDownload';
|
||||||
|
|
||||||
|
interface GridImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
|
image: NostrImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridImage = ({ image, ...props }: GridImageProps) => {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
data-node-id={image.noteId}
|
||||||
|
onError={(e: SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
e.currentTarget.src = '/notfound.png';
|
||||||
|
}}
|
||||||
|
className={`image ${loaded ? 'show' : ''}`}
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
loading="lazy"
|
||||||
|
key={image.url}
|
||||||
|
src={createImgProxyUrl(image.url)}
|
||||||
|
{...props}
|
||||||
|
></img>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GridImage;
|
@ -1,10 +1,27 @@
|
|||||||
|
@keyframes showImage {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gridview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.imagegrid {
|
.imagegrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -15,6 +32,15 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagegrid .image.show {
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
animation-name: showImage;
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import Settings from '../Settings';
|
import { NostrImage, isVideo } from '../nostrImageDownload';
|
||||||
import { NostrImage, createImgProxyUrl, isVideo } from '../nostrImageDownload';
|
|
||||||
import './GridView.css';
|
import './GridView.css';
|
||||||
import Slide from '../SlideView/Slide';
|
import DetailsView from './DetailsView';
|
||||||
|
import GridImage from './GridImage';
|
||||||
|
import { Settings } from '../../utils/useNav';
|
||||||
|
|
||||||
type GridViewProps = {
|
type GridViewProps = {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
@ -10,7 +11,7 @@ type GridViewProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GridView = ({ settings, images }: GridViewProps) => {
|
const GridView = ({ settings, images }: GridViewProps) => {
|
||||||
const [activeImage, setActiveImage] = useState<NostrImage | undefined>();
|
const [activeImageIdx, setActiveImageIdx] = useState<number | undefined>();
|
||||||
|
|
||||||
const sortedImages = useMemo(
|
const sortedImages = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -20,20 +21,33 @@ const GridView = ({ settings, images }: GridViewProps) => {
|
|||||||
[images]
|
[images]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
console.log(event);
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
setActiveImageIdx(idx => (idx !== undefined && idx < sortedImages.length - 1 ? idx + 1 : idx));
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
setActiveImageIdx(idx => (idx !== undefined && idx > 0 ? idx - 1 : idx));
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setActiveImageIdx(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="gridview">
|
||||||
{activeImage && (
|
{activeImageIdx !== undefined ? (
|
||||||
<Slide
|
<DetailsView images={sortedImages} activeImageIdx={activeImageIdx} setActiveImageIdx={setActiveImageIdx} />
|
||||||
url={activeImage.url}
|
) : null}
|
||||||
noteId={activeImage.noteId}
|
|
||||||
type={activeImage.type}
|
|
||||||
paused={false}
|
|
||||||
onAnimationEnded={() => setActiveImage(undefined)}
|
|
||||||
animationDuration={4}
|
|
||||||
></Slide>
|
|
||||||
)}
|
|
||||||
<div className="imagegrid">
|
<div className="imagegrid">
|
||||||
{sortedImages.map(image =>
|
{sortedImages.map((image, idx) =>
|
||||||
isVideo(image.url) ? (
|
isVideo(image.url) ? (
|
||||||
<video
|
<video
|
||||||
className="image"
|
className="image"
|
||||||
@ -44,18 +58,11 @@ const GridView = ({ settings, images }: GridViewProps) => {
|
|||||||
preload="none"
|
preload="none"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<GridImage key={image.url} image={image} onClick={() => setActiveImageIdx(idx)}></GridImage>
|
||||||
onClick={() => setActiveImage(image)}
|
|
||||||
data-node-id={image.noteId}
|
|
||||||
className="image"
|
|
||||||
loading="lazy"
|
|
||||||
key={image.url}
|
|
||||||
src={createImgProxyUrl(image.url)}
|
|
||||||
></img>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,38 +1,29 @@
|
|||||||
import { FormEvent, useState } from 'react';
|
import { FormEvent, useState } from 'react';
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import useNav, { Settings } from '../utils/useNav';
|
||||||
|
|
||||||
type Settings = {
|
|
||||||
showNsfw: boolean;
|
|
||||||
tags: string[];
|
|
||||||
npubs: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings = ({ onClose, settings }: SettingsProps) => {
|
const SettingsDialog = ({ onClose, settings }: SettingsProps) => {
|
||||||
const [showNsfw, setShowNsfw] = useState(settings.showNsfw || false);
|
const [showNsfw, setShowNsfw] = useState(settings.showNsfw || false);
|
||||||
const [tags, setTags] = useState(settings.tags || []);
|
const [tags, setTags] = useState(settings.tags || []);
|
||||||
const [npubs, setNpubs] = useState(settings.npubs || []);
|
const [npubs, setNpubs] = useState(settings.npubs || []);
|
||||||
|
const { nav, currentSettings } = useNav();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const onSubmit = (e: FormEvent) => {
|
const onSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const nsfwPostfix = showNsfw ? '?nsfw=true' : '';
|
|
||||||
|
|
||||||
const validTags = tags.filter(t => t.length > 0);
|
const validTags = tags.filter(t => t.length > 0);
|
||||||
const validNpubs = npubs.filter(t => t.length > 0);
|
const validNpubs = npubs.filter(t => t.length > 0);
|
||||||
|
|
||||||
if (validTags.length > 0) {
|
if (validTags.length > 0) {
|
||||||
navigate(`/tags/${validTags.join('%2C')}${nsfwPostfix}`);
|
nav({ ...currentSettings, tags: validTags, npubs: [], showNsfw });
|
||||||
} else if (validNpubs.length == 1) {
|
} else if (validNpubs.length == 1) {
|
||||||
navigate(`/p/${validNpubs[0]}${nsfwPostfix}`);
|
nav({ ...currentSettings, tags: [], npubs: validNpubs, showNsfw });
|
||||||
} else {
|
} else {
|
||||||
navigate(`/${nsfwPostfix}`);
|
nav({ ...currentSettings, tags: [], npubs: [], showNsfw });
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@ -79,4 +70,4 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default SettingsDialog;
|
||||||
|
@ -57,16 +57,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.author-info a {
|
.author-info {
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-info a:hover {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.author-info {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1em;
|
bottom: 1em;
|
||||||
left: 1em;
|
left: 1em;
|
||||||
@ -85,6 +78,7 @@
|
|||||||
animation-duration: 0.5s;
|
animation-duration: 0.5s;
|
||||||
animation-timing-function: ease-in;
|
animation-timing-function: ease-in;
|
||||||
animation-name: showAuthor;
|
animation-name: showAuthor;
|
||||||
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide {
|
.slide {
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
isVideo,
|
isVideo,
|
||||||
prepareContent,
|
prepareContent,
|
||||||
} from './nostrImageDownload';
|
} from './nostrImageDownload';
|
||||||
import { defaultRelays, nfswTags, nsfwNPubs } from './env';
|
import { blockedPublicKeys, defaultRelays, nfswTags, nsfwNPubs } from './env';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import SlideView from './SlideView';
|
import SlideView from './SlideView';
|
||||||
import GridView from './GridView';
|
import GridView from './GridView';
|
||||||
@ -23,6 +23,7 @@ import IconSettings from './Icons/IconSettings';
|
|||||||
import IconPlay from './Icons/IconPlay';
|
import IconPlay from './Icons/IconPlay';
|
||||||
import IconGrid from './Icons/IconGrid';
|
import IconGrid from './Icons/IconGrid';
|
||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import useNav from '../utils/useNav';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FEATURES:
|
FEATURES:
|
||||||
@ -45,13 +46,14 @@ FEATURES:
|
|||||||
- Prevent duplicate images (shuffle? histroy?)
|
- Prevent duplicate images (shuffle? histroy?)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SlideShow = (settings: Settings) => {
|
const SlideShow = () => {
|
||||||
const { ndk, loadNdk } = useNDK();
|
const { ndk, loadNdk } = useNDK();
|
||||||
const [posts, setPosts] = useState<NDKEvent[]>([]);
|
const [posts, setPosts] = useState<NDKEvent[]>([]);
|
||||||
const images = useRef<NostrImage[]>([]);
|
const images = useRef<NostrImage[]>([]);
|
||||||
const fetchTimeoutHandle = useRef(0);
|
const fetchTimeoutHandle = useRef(0);
|
||||||
const [showGrid, setShowGrid] = useState(false);
|
const [showGrid, setShowGrid] = useState(false);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const { currentSettings: settings } = useNav();
|
||||||
|
|
||||||
const fetch = () => {
|
const fetch = () => {
|
||||||
const postSubscription = ndk.subscribe(buildFilter(settings.tags, settings.npubs));
|
const postSubscription = ndk.subscribe(buildFilter(settings.tags, settings.npubs));
|
||||||
@ -59,6 +61,7 @@ const SlideShow = (settings: Settings) => {
|
|||||||
postSubscription.on('event', (event: NDKEvent) => {
|
postSubscription.on('event', (event: NDKEvent) => {
|
||||||
setPosts(oldPosts => {
|
setPosts(oldPosts => {
|
||||||
if (
|
if (
|
||||||
|
!blockedPublicKeys.includes(event.pubkey.toLowerCase()) && // remove blocked authors
|
||||||
!isReply(event) &&
|
!isReply(event) &&
|
||||||
oldPosts.findIndex(p => p.id === event.id) === -1 && // not duplicate
|
oldPosts.findIndex(p => p.id === event.id) === -1 && // not duplicate
|
||||||
(settings.showNsfw || !isNsfwRelated(event))
|
(settings.showNsfw || !isNsfwRelated(event))
|
||||||
@ -112,9 +115,12 @@ const SlideShow = (settings: Settings) => {
|
|||||||
if (event.key === 'g' || event.key === 'G') {
|
if (event.key === 'g' || event.key === 'G') {
|
||||||
setShowGrid(p => !p);
|
setShowGrid(p => !p);
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 's' || event.key === 'S') {
|
||||||
setShowSettings(s => !s);
|
setShowSettings(s => !s);
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setShowSettings(false);
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
if (event.key === "f" || event.key === "F") {
|
if (event.key === "f" || event.key === "F") {
|
||||||
document?.getElementById("root")?.requestFullscreen();
|
document?.getElementById("root")?.requestFullscreen();
|
||||||
@ -124,9 +130,9 @@ const SlideShow = (settings: Settings) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNdk(defaultRelays);
|
loadNdk(defaultRelays);
|
||||||
window.addEventListener('keydown', onKeyDown);
|
document.body.addEventListener('keydown', onKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
document.body.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -163,7 +169,7 @@ const SlideShow = (settings: Settings) => {
|
|||||||
{showGrid ? (
|
{showGrid ? (
|
||||||
<GridView images={images.current} settings={settings}></GridView>
|
<GridView images={images.current} settings={settings}></GridView>
|
||||||
) : (
|
) : (
|
||||||
<SlideView images={images.current} settings={settings}></SlideView>
|
<SlideView images={images.current} settings={settings} setShowGrid={setShowGrid}></SlideView>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,16 +7,17 @@ import { useNDK } from '@nostr-dev-kit/ndk-react';
|
|||||||
import useDebouncedEffect from '../../utils/useDebouncedEffect';
|
import useDebouncedEffect from '../../utils/useDebouncedEffect';
|
||||||
import { useSwipeable } from 'react-swipeable';
|
import { useSwipeable } from 'react-swipeable';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import Settings from '../Settings';
|
|
||||||
import IconPause from '../Icons/IconPause';
|
import IconPause from '../Icons/IconPause';
|
||||||
import IconSpinner from '../Icons/IconSpinner';
|
import IconSpinner from '../Icons/IconSpinner';
|
||||||
|
import { Settings } from '../../utils/useNav';
|
||||||
|
|
||||||
type SlideViewProps = {
|
type SlideViewProps = {
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
images: NostrImage[];
|
images: NostrImage[];
|
||||||
|
setShowGrid: (showGrid: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SlideView = ({ settings, images }: SlideViewProps) => {
|
const SlideView = ({ settings, images, setShowGrid }: SlideViewProps) => {
|
||||||
const { getProfile } = useNDK();
|
const { getProfile } = useNDK();
|
||||||
const [activeImages, setActiveImages] = useState<NostrImage[]>([]);
|
const [activeImages, setActiveImages] = useState<NostrImage[]>([]);
|
||||||
const history = useRef<NostrImage[]>([]);
|
const history = useRef<NostrImage[]>([]);
|
||||||
@ -130,7 +131,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.addEventListener('keydown', onKeyDown);
|
document.body.addEventListener('keydown', onKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', onKeyDown);
|
document.body.removeEventListener('keydown', onKeyDown);
|
||||||
console.log(`cleaining timeout in useEffect[] destructor `);
|
console.log(`cleaining timeout in useEffect[] destructor `);
|
||||||
clearTimeout(viewTimeoutHandle.current);
|
clearTimeout(viewTimeoutHandle.current);
|
||||||
};
|
};
|
||||||
@ -189,6 +190,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
|
|||||||
)}
|
)}
|
||||||
{activeProfile && (
|
{activeProfile && (
|
||||||
<AuthorProfile
|
<AuthorProfile
|
||||||
|
setShowGrid={setShowGrid}
|
||||||
src={urlFix(activeProfile.image || '')}
|
src={urlFix(activeProfile.image || '')}
|
||||||
author={activeProfile.displayName || activeProfile.name}
|
author={activeProfile.displayName || activeProfile.name}
|
||||||
npub={activeNpub}
|
npub={activeNpub}
|
||||||
|
@ -45,6 +45,7 @@ export const nfswTags = [
|
|||||||
'thighstr',
|
'thighstr',
|
||||||
'tits',
|
'tits',
|
||||||
'titstr',
|
'titstr',
|
||||||
|
'lolita',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const nsfwNPubs = [
|
export const nsfwNPubs = [
|
||||||
@ -78,13 +79,21 @@ export const nsfwNPubs = [
|
|||||||
'npub1ulafm4d3n7ukl7yzg4hfnhfjut74nym5p83e3d67l3j62yc6ysqqrancw2', // naked
|
'npub1ulafm4d3n7ukl7yzg4hfnhfjut74nym5p83e3d67l3j62yc6ysqqrancw2', // naked
|
||||||
'npub1ve4ztpqvlgu3v6hgrvc4lrdl2ernue7lq2h8tcgaksrkxlm7gnsqkjmz4e', // bluntkaraoke
|
'npub1ve4ztpqvlgu3v6hgrvc4lrdl2ernue7lq2h8tcgaksrkxlm7gnsqkjmz4e', // bluntkaraoke
|
||||||
'npub1wmsn8fch7kwt987jcdx06uuapn6pwzau59pvy0ql5d3xlmnxa2csj3c5p4', // StefsPicks
|
'npub1wmsn8fch7kwt987jcdx06uuapn6pwzau59pvy0ql5d3xlmnxa2csj3c5p4', // StefsPicks
|
||||||
'npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv', // Erandis Vol
|
|
||||||
'npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0', // sexy-models
|
'npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0', // sexy-models
|
||||||
'npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq', // Welcome To The Jungle
|
'npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq', // Welcome To The Jungle
|
||||||
'npub1csk2wg33ee9kutyps4nmevyv3putfegj7yd0emsp44ph32wvmamqs7uyan', // Lilura
|
'npub1csk2wg33ee9kutyps4nmevyv3putfegj7yd0emsp44ph32wvmamqs7uyan', // Lilura
|
||||||
|
'npub10m75ad8pc6wtlt67f6wjeug4hpqurc68842ve5ne47u9lkjqa0lq8ja88s', // 313Chris:hellokitty_headbang:
|
||||||
];
|
];
|
||||||
|
|
||||||
export const nsfwPubKeys = nsfwNPubs.map(npub => (nip19.decode(npub).data as string).toLowerCase());
|
export const nsfwPublicKeys = nsfwNPubs.map(npub => (nip19.decode(npub).data as string).toLowerCase());
|
||||||
|
|
||||||
|
export const blockedNPubs = [
|
||||||
|
'npub1awxh85c5wasj60d42uvmzuza2uvjazff9m7skg2vf7x2f8gykwkqykxktf', // AIイラスト',
|
||||||
|
'npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv', // Erandis Vol
|
||||||
|
'npub1kf8sau5dejmcmfmzzj256rv728p5w7s0wytdyz8ypa0ne0y6k0vswhgu9w', // noname
|
||||||
|
];
|
||||||
|
|
||||||
|
export const blockedPublicKeys = blockedNPubs.map(npub => (nip19.decode(npub).data as string).toLowerCase());
|
||||||
|
|
||||||
export const spamAccounts = [];
|
export const spamAccounts = [];
|
||||||
|
|
||||||
@ -97,5 +106,6 @@ export const defaultRelays = [
|
|||||||
'wss://nostr.wine',
|
'wss://nostr.wine',
|
||||||
// "wss://nostr1.current.fyi/",
|
// "wss://nostr1.current.fyi/",
|
||||||
'wss://purplepag.es/', // needed for user profiles
|
'wss://purplepag.es/', // needed for user profiles
|
||||||
|
'wss://n-word.sharivegas.com/', // needed for mostr.pub profiles
|
||||||
//"wss://feeds.nostr.band/pics",
|
//"wss://feeds.nostr.band/pics",
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { nfswTags, nsfwPubKeys } from './env';
|
import { nfswTags, nsfwPublicKeys } from './env';
|
||||||
|
|
||||||
export type NostrImage = {
|
export type NostrImage = {
|
||||||
url: string;
|
url: string;
|
||||||
@ -67,7 +67,7 @@ export const isNsfwRelated = (event: NDKEvent) => {
|
|||||||
return (
|
return (
|
||||||
hasContentWarning(event) || // block content warning
|
hasContentWarning(event) || // block content warning
|
||||||
hasNsfwTag(event) || // block nsfw tags
|
hasNsfwTag(event) || // block nsfw tags
|
||||||
nsfwPubKeys.includes(event.pubkey.toLowerCase()) // block nsfw authors
|
nsfwPublicKeys.includes(event.pubkey.toLowerCase()) // block nsfw authors
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
50
src/utils/useNav.ts
Normal file
50
src/utils/useNav.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { defaultHashTags } from '../components/env';
|
||||||
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
showNsfw: boolean;
|
||||||
|
tags: string[];
|
||||||
|
npubs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useNav = () => {
|
||||||
|
const { tags, npub } = useParams();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const currentSettings: Settings = useMemo(() => {
|
||||||
|
const nsfw = searchParams.get('nsfw') === 'true';
|
||||||
|
|
||||||
|
console.log(`tags = ${tags}, npub = ${npub}, nsfw = ${nsfw}`);
|
||||||
|
|
||||||
|
let useTags = tags?.split(',') || [];
|
||||||
|
if (npub == undefined && (useTags == undefined || useTags.length == 0)) {
|
||||||
|
useTags = defaultHashTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: useTags,
|
||||||
|
npubs: npub ? [npub] : [],
|
||||||
|
showNsfw: nsfw,
|
||||||
|
};
|
||||||
|
}, [tags, npub, searchParams]);
|
||||||
|
|
||||||
|
const nav = (settings: Settings) => {
|
||||||
|
const nsfwPostfix = settings.showNsfw ? '?nsfw=true' : '';
|
||||||
|
const validTags = settings.tags.filter(t => t.length > 0);
|
||||||
|
const validNpubs = settings.npubs.filter(t => t.length > 0);
|
||||||
|
|
||||||
|
if (validTags.length > 0) {
|
||||||
|
navigate(`/tags/${validTags.join('%2C')}${nsfwPostfix}`);
|
||||||
|
} else if (validNpubs.length == 1) {
|
||||||
|
navigate(`/p/${validNpubs[0]}${nsfwPostfix}`);
|
||||||
|
} else {
|
||||||
|
navigate(`/${nsfwPostfix}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { nav, currentSettings };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useNav;
|
Loading…
Reference in New Issue
Block a user