diff --git a/package-lock.json b/package-lock.json
index 92e78cd..ed24aa0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,12 +14,14 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
- "react-router-dom": "^6.14.1"
+ "react-router-dom": "^6.14.1",
+ "react-swipeable": "^7.0.1"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/react-helmet": "^6.1.6",
+ "@types/react-swipeable": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
@@ -1140,6 +1142,16 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-swipeable": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@types/react-swipeable/-/react-swipeable-5.2.0.tgz",
+ "integrity": "sha512-aQMubLpV45W8fTQufnm5j8yxYVEp/d3JJkqpPr9xcRPQ6Q6MSJUdNpsaR2uogILSIFzrAisC8AqdR1JlvjuZMA==",
+ "deprecated": "This is a stub types definition. react-swipeable provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "react-swipeable": "*"
+ }
+ },
"node_modules/@types/scheduler": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
@@ -4402,6 +4414,14 @@
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-swipeable": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.1.tgz",
+ "integrity": "sha512-RKB17JdQzvECfnVj9yDZsiYn3vH0eyva/ZbrCZXZR0qp66PBRhtg4F9yJcJTWYT5Adadi+x4NoG53BxKHwIYLQ==",
+ "peerDependencies": {
+ "react": "^16.8.3 || ^17 || ^18"
+ }
+ },
"node_modules/read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
diff --git a/package.json b/package.json
index f66eb63..66ad4a9 100644
--- a/package.json
+++ b/package.json
@@ -16,12 +16,14 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
- "react-router-dom": "^6.14.1"
+ "react-router-dom": "^6.14.1",
+ "react-swipeable": "^7.0.1"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/react-helmet": "^6.1.6",
+ "@types/react-swipeable": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
diff --git a/src/App.tsx b/src/App.tsx
index ace4d9c..cce4eb2 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,9 +4,9 @@ import "./App.css";
const App = () => {
const { tags, npub } = useParams();
- const [ searchParams ] = useSearchParams();
+ const [searchParams] = useSearchParams();
const nsfw = searchParams.get("nsfw") === "true";
-
+ console.log(`tags = ${tags}, npub = ${npub}, nsfw = ${nsfw}`);
return ;
};
diff --git a/src/components/Settings.css b/src/components/Settings.css
index 317d580..9b91eca 100644
--- a/src/components/Settings.css
+++ b/src/components/Settings.css
@@ -25,9 +25,6 @@
animation: fadeIn 0.5s ease-in-out;
z-index: 500;
padding: 2em;
-}
-
-.settings form {
display: flex;
flex-direction: column;
gap: 24px;
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index f7d3a72..2ddd843 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -2,15 +2,25 @@ import { FormEvent, useState } from "react";
import "./Settings.css";
import { useNavigate, useSearchParams } from "react-router-dom";
-const Settings = () => {
- const [showNsfw, setShowNsfw] = useState(false);
+
+type Settings = {
+ showNsfw: boolean;
+
+}
+type SettingsProps = {
+ onClose: () => void;
+ settings: Settings;
+
+};
+
+const Settings = ({onClose, settings} : SettingsProps) => {
+ const [showNsfw, setShowNsfw] = useState(settings.showNsfw);
const navigate = useNavigate();
- const [_, setSearchParams] = useSearchParams();
const onSubmit = (e: FormEvent) => {
e.preventDefault();
- //navigate(`/tags/foodstr?nsfw=${showNsfw}`);
- setSearchParams({ nsfw: showNsfw.toString() });
+ navigate(`${window.location.pathname}?nsfw=${showNsfw}`);
+ onClose();
};
return (
@@ -38,7 +48,7 @@ const Settings = () => {
diff --git a/src/components/SlideShow.css b/src/components/SlideShow.css
index eefaf63..75b1b02 100644
--- a/src/components/SlideShow.css
+++ b/src/components/SlideShow.css
@@ -115,6 +115,10 @@
z-index: 200;
}
+.controls button svg {
+ fill: white;
+}
+
.controls button {
background-color: transparent;
padding: 0.5em;
diff --git a/src/components/SlideShow.tsx b/src/components/SlideShow.tsx
index 8ee66b8..af35aed 100644
--- a/src/components/SlideShow.tsx
+++ b/src/components/SlideShow.tsx
@@ -1,6 +1,6 @@
import { useNDK } from "@nostr-dev-kit/ndk-react";
import "./SlideShow.css";
-import React, { useEffect, useRef, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
import AuthorProfile from "./AuthorProfile";
import IconFullScreen from "./IconFullScreen";
import Slide from "./Slide";
@@ -18,6 +18,7 @@ import {
} from "./nostrImageDownload";
import { appName, nsfwPubKeys } from "./env";
import Settings from "./Settings";
+import { useSwipeable } from "react-swipeable";
/*
FEATURES:
@@ -48,18 +49,18 @@ let oldest = Infinity;
let maxFetchCount = 20;
let eventsReceived = 0;
-type SlideShowProps = {
+interface SlideShowProps extends Settings {
tags?: string;
npub?: string;
showNsfw: boolean;
-};
+}
const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
const { ndk, getProfile, loadNdk } = useNDK();
const [posts, setPosts] = useState([]);
const images = useRef([]);
const [activeImages, setActiveImages] = useState([]);
- const [history, setHistory] = useState([]);
+ const history = useRef([]);
const [paused, setPaused] = useState(false);
const [showSettings, setShowSettings] = useState(false);
@@ -71,7 +72,46 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
const [activeContent, setActiveContent] = useState(
undefined
);
- const timeoutHandle = useRef(0);
+ const viewTimeoutHandle = useRef(0);
+ const fetchTimeoutHandle = useRef(0);
+
+ const queueNextImage = (waitTime = 8000) => {
+ clearTimeout(viewTimeoutHandle.current);
+ viewTimeoutHandle.current = setTimeout(() => {
+ if (!paused) {
+ setLoading(false);
+ animateImages();
+ queueNextImage();
+ }
+ }, waitTime);
+ };
+
+ const nextImage = () => {
+ setPaused(false);
+ setActiveImages([]);
+ queueNextImage(0);
+ };
+
+ const previousImage = () => {
+ setPaused(false);
+
+ console.log(history);
+ if (history.current.length > 1) {
+ const previousImage = history.current.pop(); // remove current image
+ previousImage && images.current.push(previousImage); // add current image back to the pool
+ const lastImage = history.current[history.current.length - 1]; // show preview image but leave in the history
+ if (lastImage) {
+ setActiveImages([lastImage]);
+ upcommingImage.current = lastImage;
+ queueNextImage(); // queue next image for 8s after showing this one
+ }
+ }
+ };
+
+ const swipeHandlers = useSwipeable({
+ onSwipedLeft: () => previousImage(),
+ onSwipedRight: () => nextImage(),
+ });
const fetch = () => {
const until = oldest < Infinity ? oldest : undefined;
@@ -93,13 +133,19 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
}
setPosts((oldPosts) => {
+ /*
+ console.log(oldPosts.length);
+ console.log(`received event ${event.id} ${event.created_at}`);
+ console.log(`isReply ${isReply(event)}`);
+ console.log(`showNsfw ${showNsfw} hasContentWarning ${hasContentWarning(event)} hasNsfwTag ${hasNsfwTag(event)} nsfwPubKeys ${nsfwPubKeys.includes(event.pubkey.toLowerCase())}`);
+ */
if (
!isReply(event) &&
oldPosts.findIndex((p) => p.id === event.id) === -1 &&
(showNsfw ||
(!hasContentWarning(event) && // only allow content warnings on profile content
- !hasNsfwTag(event))) && // only allow nsfw on profile content
- !nsfwPubKeys.includes(event.pubkey.toLowerCase()) // block nsfw authors
+ !hasNsfwTag(event) && // only allow nsfw on profile content
+ !nsfwPubKeys.includes(event.pubkey.toLowerCase()))) // block nsfw authors
) {
return [...oldPosts, event];
}
@@ -113,7 +159,8 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
if (maxFetchCount > 0) {
maxFetchCount--;
- setTimeout(() => {
+ clearTimeout(fetchTimeoutHandle.current);
+ fetchTimeoutHandle.current = setTimeout(() => {
console.log(JSON.stringify(untilPerRelay));
console.log(`eventsReceived ${eventsReceived}`);
@@ -132,10 +179,28 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
//"wss://feeds.nostr.band/pics"
]);
- fetch();
}, []);
+ useEffect(() => {
+ // reset all
+ console.log(`resetting`);
+ setPosts([]);
+ setPaused(false);
+ maxFetchCount = 20;
+ eventsReceived = 0;
+ setActiveImages([]);
+ history.current = [];
+ images.current = [];
+ upcommingImage.current = undefined;
+ clearTimeout(fetchTimeoutHandle.current);
+ clearTimeout(viewTimeoutHandle.current);
+
+ fetch();
+ }, [showNsfw, tags, npub]);
+
const animateImages = () => {
+ console.log(`animateImages`);
+
setActiveImages((oldImages) => {
const newActiveImages = [...oldImages];
if (newActiveImages.length > 2) {
@@ -149,7 +214,7 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
// TODO this creates potential duplicates when images are loaded from multiple relays
images.current = images.current.filter((i) => i !== randomImage);
- setHistory((oldHistory) => [...oldHistory, randomImage]);
+ history.current.push(randomImage);
newActiveImages.push(randomImage);
upcommingImage.current = randomImage;
}
@@ -187,9 +252,10 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
const onKeyDown = (event: KeyboardEvent) => {
// console.log(event);
if (event.key === "ArrowRight") {
- setPaused(false);
- setActiveImages([]);
- queueNextImage(0);
+ nextImage();
+ }
+ if (event.key === "ArrowLeft") {
+ previousImage();
}
if (event.key === "p" || event.key === " " || event.key === "P") {
setPaused((p) => !p);
@@ -203,19 +269,7 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
console.log(history);
}, [history]);
- const queueNextImage = (waitTime = 8000) => {
- clearTimeout(timeoutHandle.current);
- timeoutHandle.current = setTimeout(() => {
- if (!paused) {
- setLoading(false);
- animateImages();
- queueNextImage();
- }
- }, waitTime);
- };
-
useEffect(() => {
- queueNextImage();
document.body.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
@@ -248,15 +302,29 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
}, [activeProfile]);
return (
- <>
+
{title}
- {showSettings &&
}
+ {showSettings && (
+
setShowSettings(false)}
+ settings={{ showNsfw }}
+ >
+ )}
{!fullScreen && (
+
);
};
diff --git a/src/components/env.ts b/src/components/env.ts
index 4b5525b..3050bc7 100644
--- a/src/components/env.ts
+++ b/src/components/env.ts
@@ -63,6 +63,7 @@ export const nsfwPubKeys = [
"npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv", // Erandis Vol
"npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0", // sexy-models
"npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq", // Welcome To The Jungle
+ "npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an", // Aeontropy
].map((npub) => (nip19.decode(npub).data as string).toLowerCase());
diff --git a/src/components/nostrImageDownload.ts b/src/components/nostrImageDownload.ts
index b99e2f4..345628a 100644
--- a/src/components/nostrImageDownload.ts
+++ b/src/components/nostrImageDownload.ts
@@ -1,6 +1,6 @@
import { NDKFilter } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
-import { appName, defaultHashTags, nfswTags } from "./env";
+import { appName, nfswTags } from "./env";
export type NostrImage = {
url: string;
diff --git a/src/main.tsx b/src/main.tsx
index 7e67787..19d9567 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -29,9 +29,7 @@ const router = createBrowserRouter([
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
-
-
-
+
+
+
);