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( - - - - - + + + );