From 13a61bce9cdd1598c99498a7a0f38b8ad58d4bc8 Mon Sep 17 00:00:00 2001 From: Florian Maul Date: Sat, 29 Jul 2023 13:44:56 +0200 Subject: [PATCH] feat: Added details dialog --- public/notfound.png | Bin 0 -> 5490 bytes src/App.tsx | 15 +--- src/components/AdultContentInfo.tsx | 9 +-- src/components/AuthorProfile.tsx | 38 ++++++---- src/components/GridView/Author.tsx | 37 +++++++++ src/components/GridView/DetailsView.css | 96 ++++++++++++++++++++++++ src/components/GridView/DetailsView.tsx | 57 ++++++++++++++ src/components/GridView/GridImage.tsx | 27 +++++++ src/components/GridView/GridView.css | 28 ++++++- src/components/GridView/index.tsx | 59 ++++++++------- src/components/Settings.tsx | 23 ++---- src/components/SlideShow.css | 10 +-- src/components/SlideShow.tsx | 18 +++-- src/components/SlideView/index.tsx | 8 +- src/components/env.ts | 14 +++- src/components/nostrImageDownload.ts | 4 +- src/utils/useNav.ts | 50 ++++++++++++ 17 files changed, 395 insertions(+), 98 deletions(-) create mode 100644 public/notfound.png create mode 100644 src/components/GridView/Author.tsx create mode 100644 src/components/GridView/DetailsView.css create mode 100644 src/components/GridView/DetailsView.tsx create mode 100644 src/components/GridView/GridImage.tsx create mode 100644 src/utils/useNav.ts diff --git a/public/notfound.png b/public/notfound.png new file mode 100644 index 0000000000000000000000000000000000000000..700c62784aa1d3a2348688146c90ae92d6b39a1c GIT binary patch literal 5490 zcmd^DX;hQf)&@(N6hSIdK_$lG1eur<30wq0KtLHn6oI@XZ!io=Oag>St{|dPtrh}x z0Gtqk2vLiu2sA1<6|5+e2oxuV$|O@5^PK>q?Xvgt-gVddetaylIPW?8>}T)iJbRxv z9Ng;apr)#$DkmqW=D69;T~1E^sO+OW56q}HpE(KssIWJ0=gP^USIIu|{A}xRIXT69 z3{NtT?6QSGWrd<48Y=)s^F!IdT29W=n$L!)!7vXI00%OdL}YJCITFF35s}`eE?5_~ zExe1dIf?^&M7erWqk^e;8q(SdVaX=|2ca+zLhwUFm|Oy%h@AFI0AtxS28oz9;RO?s zHZnj2*<~xjmc@Y)cr*@0#bR*?b37WdpqZGP8k^t^5k^=eV+__9V`PlN856Lk1QS!l z><xMn))Lf#OCoc@Q7P z|eq!Omn6%ARw}b}91Bbw&Fq6jxwzxT4_AVBW#ofjF8`pEoe*ysPy12|?d^VQQ&^ZV$Z&Nr( zW0sK5T5~-k*)YZ(=CZ;#RCrT32y>Mz8#cj~14BF($CJeh`EXWSKZJ}hGC`Xm*1AAc z22+;Ax=*&ib`TFHB4y`>!kVLu@SZp$0@j3JY;1r91MG~c3ya2}M}F4Sf`G&S$rR{@ z2JxW()tE*l&{>>N2&Bvig#uv=n;D2i%n?bjWreUf02oAPJROCL3&D}e}W?s zf~?UD28{rjTNs;|Veu%Kj|BTXU zEGqEv5fx`f55Uqb0)QiP7-eo^f=5Bd_yCkK4oAlt(@f1w1E3Gy+&BzSY*5IjUS(0y zfJbAjg*gtU(@>^3Q#0TUYlebMOpH+GRJw%))r>}mO(2<`KO%d=eBKr)66SDc3uPk| zI6Deqe8S7LjVlxicww@52s$f_N&A$y>2;a2PuDTP6Gm1mGfpvo13N8P!e#~+NGWm_ z+ymbIF^p-A5+JIq_J~NTOmr|npP6QS=5)S@huQrRyI|n-Ur2EVhRdS!A|MWI69`W7 z3&bwVU6wjv@h{=CnBg$zlLXih2k^=RM?gf7QKgNme<3UTYJ3a%#6W= zYz!upmwe%(a@x$EY4iVJ|12_sJd647;{Q4Fe^MG%zcBDWZ~RNjU!1j^WGIF%;k`I^@YbXT6@SDoRy5M0iWhEIwR$daH z_}Ag$`kn_j``t6By*lIExpRptQ15D!lamJq2D~o$k&BP!Jb3WH<)oQF zGL)U2t&QMJb1vlE$Z79G*EUU6uk|JA)5x2&`hBb)FL_p$eijY=wQC2`U{)*jTxS0&hg zlthmd077l`gyAL2!{3S6ApJe_79IKK&Q=4%eSN++IN2SxiC%=TE>FwT`CW(5(5)pq zcJ|=Db{1Xq6%jJ3!3o{Lc|VcApXZuowLIr%dSGCdWu$vYgj=R1-pa~vOqqF@o?)>Y ziS{oENA3A>EjeDFY`aw0Ak6nJB)u0gj(YJ{8^HSCKPvDq_v?1lyhJj{RNvO|%T|#> zGC}c=meOd!sq4=qCi2!qV)NRmrEw?T>)m}re7Zx6K+-Z4pFNVa&r7ZF>_GeDd-!w7 z+L$!SrK2}&P9>Z;Vjf7Rb23lqljApa$9s*o@Oxg|;MXvwRL+TOuZ~>AN)?V%RF>#k zlNl{U=`nie@xlqGou_>P?qNM4-^u$7CHtETiV<-qG!rbunuZF&mtGZ#zs|p#BM(wg zt}9j{vDgr+qWGecnrxKn{{rE`lE7iL`rDj&;dP6leHNXD) z>pyxE4pU+yYHH`N*IRg=GGx8Yw`avOhNnPUcSwKUbAzAXfo8kTB=-l(F>lLsBk93Z zd_`nW(1vfL7HQXAw3k>{l6i;A*KdiDlqVfySjwCFRQ9>0kY@Vv|3N2ip z5u{V&y(w$lLQLwy#|Aq?o314$2G)CfnK5+*a&LlZF_sD2GDEQA1`ijcNa{)fAgrfrSz(ehM_pGaK1KrU)+ArL`tI$ zxTeqJPr0mMm+~L-f*9Cy4nf?rdx)n8)*WYQ_Dvr(5^`?e|gv7+e)aq^a$Ard= z>RtdA;D_rbu*!1AXao9}mkVjr{IU|9WBYe)BldQ&H_z_0X# zwD>|!&Kxs`jk249+iv@hd6OH;Vxt$>=%fux_d0$XEFErQAe;wDCv{lO8Q@2K3SXj z(ElYAogm25ZW^3OIb~b_ZrmoT>S0-uK({DSyT;9WGWDfaT|3ZM+(ds8MTOLyAXs5s z${xpS%wJUqZl?<)6PgCc9~y1g;L{dy&Dk={Z`}_MP=kJZ^g2OwZXb_!!qNZePFCX=;DO>MqrZ}j$Y2{t04RAWCK&;yQ*3z@L z`C7l-tswQ|B@40OLI~0k0v%UA7C7mNW5deVk98G7AFhZe{`yUpE?NS~R|EdrFYc45 zB!FOD5(HP=M;$ngtJ2Tr@3=#$plglsagP!C9a~6I!%BlL?kPR-UVl)s*S)s6<(o9p z)px}AWkXJ;IImA)5PJM<^U~ka78#0lj!-hJxncf|{@WoeSG!!?_#-seEeGch3FF~$7BElvlEP=n=?74d?a zqteaX4=V;4SknV4!aavzo9==wuBUt)_Qyfww5&U#tEZoWr-!3Yv9RL^2kgrny zwyyYUu=?>=Kut1Fnx3eQak97`ET2{~FP4zuC8`@8)dbbV5(P!CAdR8KJBp^N?`R5p zapc1{GdrzLKkiK;lK<$75m*3Yaydn9uw);Dg&Fg`E#LewBd(? zfhYS%z9-kUTWt^UdbJfPtj%^#SI8e})Vn*Nbwnvn(w1Zq{|$hnimpVv7Tr1i4(?*M z+}b`^>p$xCqwNXx<%GpQMC>;wAO6+Eqo^JeT(l!4B}F-jgu0WG4?w+n?I8E35G}~%m2ysr=6BD z5>ZF@8d9^?_=|Hj6FaOgANVS%FDKE|v3TLKE{dsDdAE<${OWYJQUQBmpw8XSz3sc|Q>e=ydwa*F62=(L6^{R4X;0uBkhmEa_{ry>_4RN}+LFRM9Xw zInvsh1T?>Y9l3L;`Ek|0fz~{XG;`@fpEBMmiD}*We33r${D}gyZKL`Z-9^mSvgXHc zRjJunQB_N}X%4x#U^jRNAXU^Xi!3rZ>sz1qAI`bGuyRd)#LrE-xuU-8SNHf2dwxf2to{3MB0z-ymDl?JM_*QS bTcz^GYEIcU$n;&Z|B5 { 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 ? ( - + ) : ( )} diff --git a/src/components/AdultContentInfo.tsx b/src/components/AdultContentInfo.tsx index 3463c3b..b2873b4 100644 --- a/src/components/AdultContentInfo.tsx +++ b/src/components/AdultContentInfo.tsx @@ -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) => { e.preventDefault(); - const nsfwPostfix = '?nsfw=true'; - navigate(`${window.location.pathname}${nsfwPostfix}`); + nav({ ...currentSettings, showNsfw: true }); }; const goBack = (e: MouseEvent) => { e.preventDefault(); - navigate(`/`); + nav({ npubs: [], tags: [], showNsfw: false }); }; return ( diff --git a/src/components/AuthorProfile.tsx b/src/components/AuthorProfile.tsx index 1524cc4..c43ac30 100644 --- a/src/components/AuthorProfile.tsx +++ b/src/components/AuthorProfile.tsx @@ -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 ( -
- -
- {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;