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 './App.css';
|
||||
import Disclaimer from './components/Disclaimer';
|
||||
import useDisclaimerState from './utils/useDisclaimerState';
|
||||
import { defaultHashTags } from './components/env';
|
||||
|
||||
const App = () => {
|
||||
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 (
|
||||
<>
|
||||
{disclaimerAccepted ? (
|
||||
<SlideShow tags={useTags} npubs={npub ? [npub] : []} showNsfw={nsfw} />
|
||||
<SlideShow />
|
||||
) : (
|
||||
<Disclaimer disclaimerAccepted={disclaimerAccepted} setDisclaimerAccepted={setDisclaimerAccepted} />
|
||||
)}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './Disclaimer.css';
|
||||
import { MouseEvent } from 'react';
|
||||
import useNav from '../utils/useNav';
|
||||
|
||||
const AdultContentInfo = () => {
|
||||
const navigate = useNavigate();
|
||||
const { nav, currentSettings } = useNav();
|
||||
|
||||
const proceed = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const nsfwPostfix = '?nsfw=true';
|
||||
navigate(`${window.location.pathname}${nsfwPostfix}`);
|
||||
nav({ ...currentSettings, showNsfw: true });
|
||||
};
|
||||
const goBack = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
navigate(`/`);
|
||||
nav({ npubs: [], tags: [], showNsfw: false });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,29 +1,37 @@
|
||||
import './SlideShow.css';
|
||||
import useImageLoaded from '../utils/useImageLoaded';
|
||||
import { createImgProxyUrl } from './nostrImageDownload';
|
||||
import useNav from '../utils/useNav';
|
||||
|
||||
type AvatarImageProps = {
|
||||
src?: string;
|
||||
author?: 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 { nav, currentSettings } = useNav();
|
||||
return (
|
||||
<div className="author-info">
|
||||
<a href={`https://nostrapp.link/#${npub}`} target="_blank">
|
||||
<div>
|
||||
{avatarLoaded && (
|
||||
<div
|
||||
className="author-image"
|
||||
style={{
|
||||
backgroundImage: `url(${src})`,
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{author}
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
className="author-info"
|
||||
onClick={() => {
|
||||
setShowGrid && setShowGrid(true);
|
||||
npub && nav({ ...currentSettings, tags: [], npubs: [npub] });
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{avatarLoaded && (
|
||||
<div
|
||||
className="author-image"
|
||||
style={{
|
||||
backgroundImage: src ? `url(${createImgProxyUrl(src, 80, 80)})` : 'none',
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{author}
|
||||
</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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-gap: 1rem;
|
||||
padding: 1rem;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -15,6 +32,15 @@
|
||||
object-fit: cover;
|
||||
height: 200px;
|
||||
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) {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import Settings from '../Settings';
|
||||
import { NostrImage, createImgProxyUrl, isVideo } from '../nostrImageDownload';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { NostrImage, isVideo } from '../nostrImageDownload';
|
||||
import './GridView.css';
|
||||
import Slide from '../SlideView/Slide';
|
||||
import DetailsView from './DetailsView';
|
||||
import GridImage from './GridImage';
|
||||
import { Settings } from '../../utils/useNav';
|
||||
|
||||
type GridViewProps = {
|
||||
settings: Settings;
|
||||
@ -10,7 +11,7 @@ type GridViewProps = {
|
||||
};
|
||||
|
||||
const GridView = ({ settings, images }: GridViewProps) => {
|
||||
const [activeImage, setActiveImage] = useState<NostrImage | undefined>();
|
||||
const [activeImageIdx, setActiveImageIdx] = useState<number | undefined>();
|
||||
|
||||
const sortedImages = useMemo(
|
||||
() =>
|
||||
@ -20,20 +21,33 @@ const GridView = ({ settings, images }: GridViewProps) => {
|
||||
[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 (
|
||||
<>
|
||||
{activeImage && (
|
||||
<Slide
|
||||
url={activeImage.url}
|
||||
noteId={activeImage.noteId}
|
||||
type={activeImage.type}
|
||||
paused={false}
|
||||
onAnimationEnded={() => setActiveImage(undefined)}
|
||||
animationDuration={4}
|
||||
></Slide>
|
||||
)}
|
||||
<div className="gridview">
|
||||
{activeImageIdx !== undefined ? (
|
||||
<DetailsView images={sortedImages} activeImageIdx={activeImageIdx} setActiveImageIdx={setActiveImageIdx} />
|
||||
) : null}
|
||||
<div className="imagegrid">
|
||||
{sortedImages.map(image =>
|
||||
{sortedImages.map((image, idx) =>
|
||||
isVideo(image.url) ? (
|
||||
<video
|
||||
className="image"
|
||||
@ -44,18 +58,11 @@ const GridView = ({ settings, images }: GridViewProps) => {
|
||||
preload="none"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
onClick={() => setActiveImage(image)}
|
||||
data-node-id={image.noteId}
|
||||
className="image"
|
||||
loading="lazy"
|
||||
key={image.url}
|
||||
src={createImgProxyUrl(image.url)}
|
||||
></img>
|
||||
<GridImage key={image.url} image={image} onClick={() => setActiveImageIdx(idx)}></GridImage>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,38 +1,29 @@
|
||||
import { FormEvent, useState } from 'react';
|
||||
import './Settings.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
type Settings = {
|
||||
showNsfw: boolean;
|
||||
tags: string[];
|
||||
npubs: string[];
|
||||
};
|
||||
import useNav, { Settings } from '../utils/useNav';
|
||||
|
||||
type SettingsProps = {
|
||||
onClose: () => void;
|
||||
settings: Settings;
|
||||
};
|
||||
|
||||
const Settings = ({ onClose, settings }: SettingsProps) => {
|
||||
const SettingsDialog = ({ onClose, settings }: SettingsProps) => {
|
||||
const [showNsfw, setShowNsfw] = useState(settings.showNsfw || false);
|
||||
const [tags, setTags] = useState(settings.tags || []);
|
||||
const [npubs, setNpubs] = useState(settings.npubs || []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { nav, currentSettings } = useNav();
|
||||
|
||||
const onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const nsfwPostfix = showNsfw ? '?nsfw=true' : '';
|
||||
|
||||
const validTags = tags.filter(t => t.length > 0);
|
||||
const validNpubs = npubs.filter(t => t.length > 0);
|
||||
|
||||
if (validTags.length > 0) {
|
||||
navigate(`/tags/${validTags.join('%2C')}${nsfwPostfix}`);
|
||||
nav({ ...currentSettings, tags: validTags, npubs: [], showNsfw });
|
||||
} else if (validNpubs.length == 1) {
|
||||
navigate(`/p/${validNpubs[0]}${nsfwPostfix}`);
|
||||
nav({ ...currentSettings, tags: [], npubs: validNpubs, showNsfw });
|
||||
} else {
|
||||
navigate(`/${nsfwPostfix}`);
|
||||
nav({ ...currentSettings, tags: [], npubs: [], showNsfw });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
@ -79,4 +70,4 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
export default SettingsDialog;
|
||||
|
@ -57,16 +57,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.author-info a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.author-info a:hover {
|
||||
.author-info {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
@ -85,6 +78,7 @@
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: ease-in;
|
||||
animation-name: showAuthor;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.slide {
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
isVideo,
|
||||
prepareContent,
|
||||
} from './nostrImageDownload';
|
||||
import { defaultRelays, nfswTags, nsfwNPubs } from './env';
|
||||
import { blockedPublicKeys, defaultRelays, nfswTags, nsfwNPubs } from './env';
|
||||
import Settings from './Settings';
|
||||
import SlideView from './SlideView';
|
||||
import GridView from './GridView';
|
||||
@ -23,6 +23,7 @@ import IconSettings from './Icons/IconSettings';
|
||||
import IconPlay from './Icons/IconPlay';
|
||||
import IconGrid from './Icons/IconGrid';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import useNav from '../utils/useNav';
|
||||
|
||||
/*
|
||||
FEATURES:
|
||||
@ -45,13 +46,14 @@ FEATURES:
|
||||
- Prevent duplicate images (shuffle? histroy?)
|
||||
*/
|
||||
|
||||
const SlideShow = (settings: Settings) => {
|
||||
const SlideShow = () => {
|
||||
const { ndk, loadNdk } = useNDK();
|
||||
const [posts, setPosts] = useState<NDKEvent[]>([]);
|
||||
const images = useRef<NostrImage[]>([]);
|
||||
const fetchTimeoutHandle = useRef(0);
|
||||
const [showGrid, setShowGrid] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const { currentSettings: settings } = useNav();
|
||||
|
||||
const fetch = () => {
|
||||
const postSubscription = ndk.subscribe(buildFilter(settings.tags, settings.npubs));
|
||||
@ -59,6 +61,7 @@ const SlideShow = (settings: Settings) => {
|
||||
postSubscription.on('event', (event: NDKEvent) => {
|
||||
setPosts(oldPosts => {
|
||||
if (
|
||||
!blockedPublicKeys.includes(event.pubkey.toLowerCase()) && // remove blocked authors
|
||||
!isReply(event) &&
|
||||
oldPosts.findIndex(p => p.id === event.id) === -1 && // not duplicate
|
||||
(settings.showNsfw || !isNsfwRelated(event))
|
||||
@ -112,9 +115,12 @@ const SlideShow = (settings: Settings) => {
|
||||
if (event.key === 'g' || event.key === 'G') {
|
||||
setShowGrid(p => !p);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
if (event.key === 's' || event.key === 'S') {
|
||||
setShowSettings(s => !s);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
setShowSettings(false);
|
||||
}
|
||||
/*
|
||||
if (event.key === "f" || event.key === "F") {
|
||||
document?.getElementById("root")?.requestFullscreen();
|
||||
@ -124,9 +130,9 @@ const SlideShow = (settings: Settings) => {
|
||||
|
||||
useEffect(() => {
|
||||
loadNdk(defaultRelays);
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
document.body.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
document.body.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -163,7 +169,7 @@ const SlideShow = (settings: Settings) => {
|
||||
{showGrid ? (
|
||||
<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 { useSwipeable } from 'react-swipeable';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Settings from '../Settings';
|
||||
import IconPause from '../Icons/IconPause';
|
||||
import IconSpinner from '../Icons/IconSpinner';
|
||||
import { Settings } from '../../utils/useNav';
|
||||
|
||||
type SlideViewProps = {
|
||||
settings: Settings;
|
||||
images: NostrImage[];
|
||||
setShowGrid: (showGrid: boolean) => void;
|
||||
};
|
||||
|
||||
const SlideView = ({ settings, images }: SlideViewProps) => {
|
||||
const SlideView = ({ settings, images, setShowGrid }: SlideViewProps) => {
|
||||
const { getProfile } = useNDK();
|
||||
const [activeImages, setActiveImages] = useState<NostrImage[]>([]);
|
||||
const history = useRef<NostrImage[]>([]);
|
||||
@ -130,7 +131,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
|
||||
useEffect(() => {
|
||||
document.body.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
document.body.removeEventListener('keydown', onKeyDown);
|
||||
console.log(`cleaining timeout in useEffect[] destructor `);
|
||||
clearTimeout(viewTimeoutHandle.current);
|
||||
};
|
||||
@ -189,6 +190,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
|
||||
)}
|
||||
{activeProfile && (
|
||||
<AuthorProfile
|
||||
setShowGrid={setShowGrid}
|
||||
src={urlFix(activeProfile.image || '')}
|
||||
author={activeProfile.displayName || activeProfile.name}
|
||||
npub={activeNpub}
|
||||
|
@ -45,6 +45,7 @@ export const nfswTags = [
|
||||
'thighstr',
|
||||
'tits',
|
||||
'titstr',
|
||||
'lolita',
|
||||
];
|
||||
|
||||
export const nsfwNPubs = [
|
||||
@ -78,13 +79,21 @@ export const nsfwNPubs = [
|
||||
'npub1ulafm4d3n7ukl7yzg4hfnhfjut74nym5p83e3d67l3j62yc6ysqqrancw2', // naked
|
||||
'npub1ve4ztpqvlgu3v6hgrvc4lrdl2ernue7lq2h8tcgaksrkxlm7gnsqkjmz4e', // bluntkaraoke
|
||||
'npub1wmsn8fch7kwt987jcdx06uuapn6pwzau59pvy0ql5d3xlmnxa2csj3c5p4', // StefsPicks
|
||||
'npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv', // Erandis Vol
|
||||
'npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0', // sexy-models
|
||||
'npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq', // Welcome To The Jungle
|
||||
'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 = [];
|
||||
|
||||
@ -97,5 +106,6 @@ export const defaultRelays = [
|
||||
'wss://nostr.wine',
|
||||
// "wss://nostr1.current.fyi/",
|
||||
'wss://purplepag.es/', // needed for user profiles
|
||||
'wss://n-word.sharivegas.com/', // needed for mostr.pub profiles
|
||||
//"wss://feeds.nostr.band/pics",
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { nfswTags, nsfwPubKeys } from './env';
|
||||
import { nfswTags, nsfwPublicKeys } from './env';
|
||||
|
||||
export type NostrImage = {
|
||||
url: string;
|
||||
@ -67,7 +67,7 @@ export const isNsfwRelated = (event: NDKEvent) => {
|
||||
return (
|
||||
hasContentWarning(event) || // block content warning
|
||||
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