-
-
- {avatarLoaded && (
-
- )}
- {author}
-
-
+
{
+ setShowGrid && setShowGrid(true);
+ npub && nav({ ...currentSettings, tags: [], npubs: [npub] });
+ }}
+ >
+
+ {avatarLoaded && (
+
+ )}
+ {author}
+
);
};
diff --git a/src/components/GridView/Author.tsx b/src/components/GridView/Author.tsx
new file mode 100644
index 0000000..7091e79
--- /dev/null
+++ b/src/components/GridView/Author.tsx
@@ -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 (
+
{
+ setActiveImageIdx(undefined);
+ npub && nav({ ...currentSettings, tags: [], npubs: [npub] });
+ }}
+ >
+ <>
+
+
+ {profile?.displayName || profile?.name}
+ >
+
+ );
+};
+
+export default DetailsAuthor;
diff --git a/src/components/GridView/DetailsView.css b/src/components/GridView/DetailsView.css
new file mode 100644
index 0000000..ec05b77
--- /dev/null
+++ b/src/components/GridView/DetailsView.css
@@ -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%;
+ }
+}
diff --git a/src/components/GridView/DetailsView.tsx b/src/components/GridView/DetailsView.tsx
new file mode 100644
index 0000000..aac6466
--- /dev/null
+++ b/src/components/GridView/DetailsView.tsx
@@ -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 (
+
+
+
+
+
+
+
{currentImage?.content}
+
+ {uniq(currentImage?.tags).map(t => (
+ <>
+ {
+ setActiveImageIdx(undefined);
+ nav({ ...currentSettings, tags: [t], npubs: [] });
+ }}
+ >
+ {t}
+ {' '}
+ >
+ ))}
+
+
+
+
+ );
+};
+
+export default DetailsView;
diff --git a/src/components/GridView/GridImage.tsx b/src/components/GridView/GridImage.tsx
new file mode 100644
index 0000000..ab8134a
--- /dev/null
+++ b/src/components/GridView/GridImage.tsx
@@ -0,0 +1,27 @@
+import { SyntheticEvent, useState } from 'react';
+import { NostrImage, createImgProxyUrl } from '../nostrImageDownload';
+
+interface GridImageProps extends React.ImgHTMLAttributes
{
+ image: NostrImage;
+}
+
+const GridImage = ({ image, ...props }: GridImageProps) => {
+ const [loaded, setLoaded] = useState(false);
+
+ return (
+ ) => {
+ e.currentTarget.src = '/notfound.png';
+ }}
+ className={`image ${loaded ? 'show' : ''}`}
+ onLoad={() => setLoaded(true)}
+ loading="lazy"
+ key={image.url}
+ src={createImgProxyUrl(image.url)}
+ {...props}
+ >
+ );
+};
+
+export default GridImage;
diff --git a/src/components/GridView/GridView.css b/src/components/GridView/GridView.css
index 6fd393e..b7f95b8 100644
--- a/src/components/GridView/GridView.css
+++ b/src/components/GridView/GridView.css
@@ -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) {
diff --git a/src/components/GridView/index.tsx b/src/components/GridView/index.tsx
index b7dae92..2052e19 100644
--- a/src/components/GridView/index.tsx
+++ b/src/components/GridView/index.tsx
@@ -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();
+ const [activeImageIdx, setActiveImageIdx] = useState();
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 && (
- setActiveImage(undefined)}
- animationDuration={4}
- >
- )}
+
+ {activeImageIdx !== undefined ? (
+
+ ) : null}
- {sortedImages.map(image =>
+ {sortedImages.map((image, idx) =>
isVideo(image.url) ? (
- >
+
);
};
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index dcbd591..93dd144 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -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;
diff --git a/src/components/SlideShow.css b/src/components/SlideShow.css
index f528baf..678d0f9 100644
--- a/src/components/SlideShow.css
+++ b/src/components/SlideShow.css
@@ -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 {
diff --git a/src/components/SlideShow.tsx b/src/components/SlideShow.tsx
index fc12354..869f664 100644
--- a/src/components/SlideShow.tsx
+++ b/src/components/SlideShow.tsx
@@ -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([]);
const images = useRef([]);
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 ? (
) : (
-
+
)}
>
);
diff --git a/src/components/SlideView/index.tsx b/src/components/SlideView/index.tsx
index 76f8685..f014eac 100644
--- a/src/components/SlideView/index.tsx
+++ b/src/components/SlideView/index.tsx
@@ -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([]);
const history = useRef([]);
@@ -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 && (
(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",
];
diff --git a/src/components/nostrImageDownload.ts b/src/components/nostrImageDownload.ts
index 643f101..8d854eb 100644
--- a/src/components/nostrImageDownload.ts
+++ b/src/components/nostrImageDownload.ts
@@ -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
);
};
diff --git a/src/utils/useNav.ts b/src/utils/useNav.ts
new file mode 100644
index 0000000..18a113b
--- /dev/null
+++ b/src/utils/useNav.ts
@@ -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;