feat: Settings dialog

This commit is contained in:
florian 2023-07-22 00:50:36 +02:00
parent f846b57dc8
commit e11233398a
10 changed files with 147 additions and 47 deletions

22
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 <SlideShow tags={tags} npub={npub} showNsfw={nsfw} />;
};

View File

@ -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;

View File

@ -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 = () => {
</div>
<div className="settings-footer">
<button type="submit" className="btn btn-primary" onClick={onSubmit}>
Save
Save Settings
</button>
</div>
</div>

View File

@ -115,6 +115,10 @@
z-index: 200;
}
.controls button svg {
fill: white;
}
.controls button {
background-color: transparent;
padding: 0.5em;

View File

@ -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<any[]>([]);
const images = useRef<NostrImage[]>([]);
const [activeImages, setActiveImages] = useState<NostrImage[]>([]);
const [history, setHistory] = useState<NostrImage[]>([]);
const history = useRef<NostrImage[]>([]);
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<string | undefined>(
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 (
<>
<div {...swipeHandlers} style={{ overflow: "hidden" }}>
<Helmet>
<title>{title}</title>
</Helmet>
{showSettings && <Settings></Settings>}
{showSettings && (
<Settings
onClose={() => setShowSettings(false)}
settings={{ showNsfw }}
></Settings>
)}
{!fullScreen && (
<div className="controls">
<button onClick={() => setShowSettings(true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 512 512"
>
<path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z" />
</svg>
</button>
<button
onClick={() =>
document?.getElementById("root")?.requestFullscreen()
@ -307,7 +375,7 @@ const SlideShow = ({ tags, npub, showNsfw = false }: SlideShowProps) => {
{activeImages.map((image) => (
<Slide key={image.url} url={image.url} paused={paused} />
))}
</>
</div>
);
};

View File

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

View File

@ -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;

View File

@ -29,9 +29,7 @@ const router = createBrowserRouter([
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<NDKProvider>
<RouterProvider router={router} />
</NDKProvider>
</React.StrictMode>
);