chore: formatting

This commit is contained in:
florian 2023-07-28 17:20:52 +02:00
parent 45181ad29e
commit a3edb311fd
26 changed files with 560 additions and 434 deletions

16
.prettierrc Normal file
View File

@ -0,0 +1,16 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"rangeStart": 0,
"rangeEnd": 9007199254740991,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve"
}

247
package-lock.json generated
View File

@ -30,8 +30,10 @@
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"prettier": "^3.0.0",
"typescript": "^5.0.2",
"vite": "^4.3.9"
"vite": "^4.3.9",
"vite-bundle-visualizer": "^0.10.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -1601,6 +1603,15 @@
"node": ">=6.14.2"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -1678,6 +1689,20 @@
"node": ">=4"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -1801,6 +1826,15 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/define-properties": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
@ -2760,6 +2794,15 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
@ -3123,6 +3166,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -3298,6 +3356,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -4061,6 +4131,23 @@
"wrappy": "1"
}
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@ -4259,6 +4346,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "29.6.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.1.tgz",
@ -4598,6 +4700,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@ -4661,6 +4772,32 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.2.tgz",
"integrity": "sha512-waHktD5mlWrYFrhOLbti4YgQCn1uR24nYsNuXxg7LkPH8KdTXVWR9DNY1WU0QqokyMixVXJS4J04HNrVTMP01A==",
"dev": true,
"dependencies": {
"open": "^8.4.0",
"picomatch": "^2.3.1",
"source-map": "^0.7.4",
"yargs": "^17.5.1"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"rollup": "2.x || 3.x"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -4782,6 +4919,15 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -5297,6 +5443,19 @@
}
}
},
"node_modules/vite-bundle-visualizer": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-0.10.0.tgz",
"integrity": "sha512-11AwKlkhvw6jjiGbTiCZqBSGg/FQDLc0mVcoLWVov2jU/Ban67l+Sk4Fa0Iyctb5sObqg/dA28HkKCEmSRjw9g==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
"rollup-plugin-visualizer": "^5.9.2"
},
"bin": {
"vite-bundle-visualizer": "bin.js"
}
},
"node_modules/vite/node_modules/@esbuild/android-arm": {
"version": "0.18.13",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.13.tgz",
@ -5780,11 +5939,70 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
@ -5799,6 +6017,24 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
@ -5807,6 +6043,15 @@
"node": ">=10"
}
},
"node_modules/yargs/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -7,7 +7,9 @@
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"format": "prettier -w src/",
"analyze": "vite-bundle-visualizer"
},
"dependencies": {
"@nostr-dev-kit/ndk": "^0.7.7",
@ -32,7 +34,9 @@
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"prettier": "^3.0.0",
"typescript": "^5.0.2",
"vite": "^4.3.9"
"vite": "^4.3.9",
"vite-bundle-visualizer": "^0.10.0"
}
}

View File

@ -1,22 +1,22 @@
import { useParams, useSearchParams } from "react-router-dom";
import SlideShow from "./components/SlideShow";
import "./App.css";
import Disclaimer from "./components/Disclaimer";
import useDisclaimerState from "./utils/useDisclaimerState";
import { defaultHashTags } from "./components/env";
import { useParams, useSearchParams } from 'react-router-dom';
import SlideShow from './components/SlideShow';
import './App.css';
import Disclaimer from './components/Disclaimer';
import useDisclaimerState from './utils/useDisclaimerState';
import { defaultHashTags } from './components/env';
const App = () => {
const { disclaimerAccepted, setDisclaimerAccepted } = useDisclaimerState();
const { tags, npub } = useParams();
const [searchParams] = useSearchParams();
const nsfw = searchParams.get("nsfw") === "true";
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);
let useTags = tags?.split(',') || [];
if (npub == undefined && (useTags == undefined || useTags.length == 0)) {
useTags = defaultHashTags;
}
return (
@ -24,10 +24,7 @@ const App = () => {
{disclaimerAccepted ? (
<SlideShow tags={useTags} npubs={npub ? [npub] : []} showNsfw={nsfw} />
) : (
<Disclaimer
disclaimerAccepted={disclaimerAccepted}
setDisclaimerAccepted={setDisclaimerAccepted}
/>
<Disclaimer disclaimerAccepted={disclaimerAccepted} setDisclaimerAccepted={setDisclaimerAccepted} />
)}
</>
);

View File

@ -1,13 +1,13 @@
import { useNavigate } from "react-router-dom";
import "./Disclaimer.css";
import { MouseEvent } from "react";
import { useNavigate } from 'react-router-dom';
import './Disclaimer.css';
import { MouseEvent } from 'react';
const AdultContentInfo = () => {
const navigate = useNavigate();
const proceed = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const nsfwPostfix = "?nsfw=true";
const nsfwPostfix = '?nsfw=true';
navigate(`${window.location.pathname}${nsfwPostfix}`);
};
const goBack = (e: MouseEvent<HTMLButtonElement>) => {
@ -18,19 +18,19 @@ const AdultContentInfo = () => {
return (
<div className="disclaimer">
<div className="disclaimer-content">
<div className="warning" style={{ textAlign: "center" }}>
NSFW or Adult Content
<div className="warning" style={{ textAlign: 'center' }}>
NSFW Content Warning
</div>
<br />
You are trying to access a user profile (npub) or a tag that is marked
as NSFW (Not Safe For Work). This means that the content you are about
to see may be offensive or inappropriate for some users. If you are
under 18 years old, please do not proceed.
You are attempting to access a user profile (npub) or a tag that has been flagged as NSFW (Not Safe For Work).
This indicates that the content you are about to view might be offensive or inappropriate for certain users. If
you are under 18 years old, we kindly request that you refrain from proceeding further.
</div>
<div className="disclaimer-footer">
<button type="submit" className="btn" onClick={proceed}>
Continue to content
</button>&nbsp;
Proceed Anyway
</button>
&nbsp;&nbsp;
<button type="submit" className="btn btn-primary" onClick={goBack}>
Go back
</button>

View File

@ -1,5 +1,5 @@
import "./SlideShow.css";
import useImageLoaded from "../utils/useImageLoaded";
import './SlideShow.css';
import useImageLoaded from '../utils/useImageLoaded';
type AvatarImageProps = {
src?: string;

View File

@ -30,7 +30,6 @@
gap: 24px;
}
.disclaimer .disclaimer-footer {
flex-shrink: 1;
display: flex;

View File

@ -1,14 +1,11 @@
import "./Disclaimer.css";
import './Disclaimer.css';
type DisclaimerProps = {
disclaimerAccepted: boolean;
setDisclaimerAccepted: (accepted: boolean) => void;
};
const Disclaimer = ({
disclaimerAccepted,
setDisclaimerAccepted,
}: DisclaimerProps) => {
const Disclaimer = ({ disclaimerAccepted, setDisclaimerAccepted }: DisclaimerProps) => {
const onSubmit = () => {
setDisclaimerAccepted(true);
};
@ -20,18 +17,15 @@ const Disclaimer = ({
return (
<div className="disclaimer">
<div className="disclaimer-content">
<div className="warning" style={{ textAlign: "center" }}>
<div className="warning" style={{ textAlign: 'center' }}>
Warning!
</div>
<br />
The content presented on this site is <b>entirely user-generated</b> and
remains <b>unmoderated</b>. Images and videos are sourced from the NOSTR
platform and are not hosted on this site. Content filtering efforts are
made to avoid NSFW (Not Safe For Work) content, but we cannot guarantee
complete safety. Please use discretion and be responsible while engaging
with the material on this platform. By using this site, you agree not to
hold the site owners, operators, and affiliates liable for any
content-related experiences.
The content presented on this site is <b>entirely user-generated</b> and remains <b>unmoderated</b>. Images and
videos are sourced from the NOSTR platform and are not hosted on this site. Content filtering efforts are made
to avoid NSFW (Not Safe For Work) content, but we cannot guarantee complete safety. Please use discretion and be
responsible while engaging with the material on this platform. By using this site, you agree not to hold the
site owners, operators, and affiliates liable for any content-related experiences.
</div>
<div className="disclaimer-footer">
<button type="submit" className="btn btn-primary" onClick={onSubmit}>

View File

@ -19,13 +19,11 @@
@media screen and (max-width: 600px) {
.imagegrid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
.imagegrid .image:hover {
filter: brightness(1.1);
outline: 1px solid #fff;
}
}

View File

@ -1,8 +1,8 @@
import { useMemo, useState } from "react";
import Settings from "../Settings";
import { NostrImage } from "../nostrImageDownload";
import "./GridView.css";
import Slide from "../SlideView/Slide";
import { useMemo, useState } from 'react';
import Settings from '../Settings';
import { NostrImage } from '../nostrImageDownload';
import './GridView.css';
import Slide from '../SlideView/Slide';
type GridViewProps = {
settings: Settings;
@ -10,24 +10,23 @@ type GridViewProps = {
};
const isVideo = (url: string) => {
return url.endsWith(".mp4") || url.endsWith(".webm");
return url.endsWith('.mp4') || url.endsWith('.webm');
};
const addProxy = (url: string) => {
if (
url.includes("imgur.com") ||
url.includes("cdn.midjourney.com") ||
url.includes("wasabisys.com") ||
url.includes("files.mastodon.social") ||
url.includes("files.mastodon.online") ||
url.includes("media.mastodon.scot") ||
url.includes("media.mas.to") ||
url.includes("smutlandia.com") ||
url.includes("file.misskey.design")
url.includes('imgur.com') ||
url.includes('cdn.midjourney.com') ||
url.includes('wasabisys.com') ||
url.includes('files.mastodon.social') ||
url.includes('files.mastodon.online') ||
url.includes('media.mastodon.scot') ||
url.includes('media.mas.to') ||
url.includes('smutlandia.com') ||
url.includes('file.misskey.design')
)
return url;
return "https://imgproxy.iris.to/insecure/rs:fill:200:200/plain/" + url;
return 'https://imgproxy.iris.to/insecure/rs:fill:200:200/plain/' + url;
};
const GridView = ({ settings, images }: GridViewProps) => {
@ -36,7 +35,7 @@ const GridView = ({ settings, images }: GridViewProps) => {
const sortedImages = useMemo(
() =>
images
.filter((i) => !isVideo(i.url)) // TODO: filter out video for now, since we don't have a good way to display them
.filter(i => !isVideo(i.url)) // TODO: filter out video for now, since we don't have a good way to display them
.sort((a, b) => b.timestamp - a.timestamp), // sort by timestamp descending
[images]
);
@ -54,7 +53,7 @@ const GridView = ({ settings, images }: GridViewProps) => {
></Slide>
)}
<div className="imagegrid">
{sortedImages.map((image) =>
{sortedImages.map(image =>
isVideo(image.url) ? (
<video
className="image"

View File

@ -1,11 +1,6 @@
const IconFullScreen = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 448 512"
fill="white"
>
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" fill="white">
<path d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z" />
</svg>
);

View File

@ -57,7 +57,7 @@
color: #aaa;
}
.settings .settings-content input[type="text"],
.settings .settings-content input[type='text'],
.settings .settings-content textarea {
display: block;
width: 100%;

View File

@ -1,6 +1,6 @@
import { FormEvent, useState } from "react";
import "./Settings.css";
import { useNavigate } from "react-router-dom";
import { FormEvent, useState } from 'react';
import './Settings.css';
import { useNavigate } from 'react-router-dom';
type Settings = {
showNsfw: boolean;
@ -22,13 +22,13 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
const onSubmit = (e: FormEvent) => {
e.preventDefault();
const nsfwPostfix = showNsfw ? "?nsfw=true" : "";
const nsfwPostfix = showNsfw ? '?nsfw=true' : '';
const validTags = tags.filter((t) => t.length > 0);
const validNpubs = npubs.filter((t) => t.length > 0);
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}`);
navigate(`/tags/${validTags.join('%2C')}${nsfwPostfix}`);
} else if (validNpubs.length == 1) {
navigate(`/p/${validNpubs[0]}${nsfwPostfix}`);
} else {
@ -38,7 +38,7 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
};
return (
<div className="settings" onClick={(e) => e.stopPropagation()}>
<div className="settings" onClick={e => e.stopPropagation()}>
<h2>Settings</h2>
<div className="settings-content">
@ -47,12 +47,8 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
name="tags"
rows={4}
id="tags"
value={tags.join(", ")}
onChange={(e) =>
setTags(
e.target.value.split(",").map((t) => t.trim().toLowerCase())
)
}
value={tags.join(', ')}
onChange={e => setTags(e.target.value.split(',').map(t => t.trim().toLowerCase()))}
></textarea>
<label htmlFor="npub">User Profile (Npub):</label>
@ -60,28 +56,15 @@ const Settings = ({ onClose, settings }: SettingsProps) => {
type="text"
name="npub"
id="npub"
value={npubs.join(", ")}
onChange={(e) =>
setNpubs(
e.target.value.split(",").map((t) => t.trim().toLowerCase())
)
}
value={npubs.join(', ')}
onChange={e => setNpubs(e.target.value.split(',').map(t => t.trim().toLowerCase()))}
/>
<div className="content-warning">
<div>
<input
name="nsfw"
type="checkbox"
checked={showNsfw}
onChange={(e) => setShowNsfw(e.target.checked)}
/>
<input name="nsfw" type="checkbox" checked={showNsfw} onChange={e => setShowNsfw(e.target.checked)} />
</div>
<label
htmlFor="nsfw"
onClick={() => setShowNsfw((n) => !n)}
style={{ userSelect: "none" }}
>
<label htmlFor="nsfw" onClick={() => setShowNsfw(n => !n)} style={{ userSelect: 'none' }}>
<div className="warning">NSFW Content</div>
Allow NSFW to be shown and ignore content warnings.
</label>

View File

@ -153,11 +153,7 @@
z-index: 200;
height: 150px;
padding-top: 120px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 100%
);
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%);
padding-left: 100px;
opacity: 0;
width: 100%;

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 { useNDK } from '@nostr-dev-kit/ndk-react';
import './SlideShow.css';
import React, { useEffect, useRef, useState } from 'react';
import {
NostrImage,
buildFilter,
@ -9,46 +9,33 @@ import {
hasNsfwTag,
isReply,
prepareContent,
} from "./nostrImageDownload";
import { nfswTags, nsfwNPubs, nsfwPubKeys } from "./env";
import Settings from "./Settings";
import SlideView from "./SlideView";
import GridView from "./GridView";
import { nip19 } from "nostr-tools";
import IconFullScreen from "./IconFullScreen";
import { uniqBy } from "lodash";
import AdultContentInfo from "./AdultContentInfo";
} from './nostrImageDownload';
import { nfswTags, nsfwNPubs, nsfwPubKeys } from './env';
import Settings from './Settings';
import SlideView from './SlideView';
import GridView from './GridView';
import { nip19 } from 'nostr-tools';
import IconFullScreen from './IconFullScreen';
import uniqBy from 'lodash/uniqBy';
import AdultContentInfo from './AdultContentInfo';
/*
FEATURES:
- dedupe urls
- controls lighter
- info for nsfw acc / tags
- preview for videos
- details view for the grid
--------
- show tags
- show content text (how to beautify?, crop?)
- show tags
- preview for videos
- jump to note
- negative hashtag filter
- login to use your own feed
- login to use your your blocked/muted list
- Keypoard shortcuts, arrow, spacebar
- jump tu next image
- jump to previous image????
- pause?
- Save-Mode and block NSFW content??
- Block certain authors / npbs? Maybe trust lookup @ nostr.band?
- Add warning start page with localStorge to remember
- Add config/settigns dialog
- Support people lists and note lists
- flag/mute button?
- Add to album button? Favorite button?
- Prevent duplicates (shuffle?), prevent same author twice in a row
- show content warning?
- Support Deleted Events
- Support reposts and replies (incl. filter in settings)
- Prevent duplicate images (shuffle? histroy?)
*/
@ -67,17 +54,15 @@ const SlideShow = (settings: Settings) => {
const fetch = () => {
eventsReceived = 0;
const postSubscription = ndk.subscribe(
buildFilter(settings.tags, settings.npubs)
);
const postSubscription = ndk.subscribe(buildFilter(settings.tags, settings.npubs));
postSubscription.on("event", (event) => {
postSubscription.on('event', event => {
eventsReceived++;
setPosts((oldPosts) => {
setPosts(oldPosts => {
if (
!isReply(event) &&
oldPosts.findIndex((p) => p.id === event.id) === -1 &&
oldPosts.findIndex(p => p.id === event.id) === -1 &&
(settings.showNsfw ||
(!hasContentWarning(event) && // only allow content warnings on profile content
!hasNsfwTag(event) && // only allow nsfw on profile content
@ -89,8 +74,8 @@ const SlideShow = (settings: Settings) => {
});
});
postSubscription.on("notice", (notice) => {
console.log("NOTICE: ", notice);
postSubscription.on('notice', notice => {
console.log('NOTICE: ', notice);
});
return () => {
@ -100,15 +85,15 @@ const SlideShow = (settings: Settings) => {
useEffect(() => {
loadNdk([
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://nos.lol",
"wss://relay.mostr.pub",
"wss://relay.shitforce.one/",
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.mostr.pub',
'wss://relay.shitforce.one/',
//"wss://nostr.wine",
// "wss://nostr1.current.fyi/",
"wss://purplepag.es/", // needed for user profiles
'wss://purplepag.es/', // needed for user profiles
//"wss://feeds.nostr.band/pics",
]);
}, []);
@ -127,32 +112,29 @@ const SlideShow = (settings: Settings) => {
useEffect(() => {
images.current = uniqBy(
posts.flatMap((p) => {
posts.flatMap(p => {
return extractImageUrls(p.content)
.filter(
(url) =>
url.endsWith(".jpg") ||
url.endsWith(".png") ||
url.endsWith(".gif") ||
url.endsWith(".jpeg") ||
url.endsWith(".webp") ||
url.endsWith(".webm") ||
url.endsWith(".mp4")
url =>
url.endsWith('.jpg') ||
url.endsWith('.png') ||
url.endsWith('.gif') ||
url.endsWith('.jpeg') ||
url.endsWith('.webp') ||
url.endsWith('.webm') ||
url.endsWith('.mp4')
)
.map((url) => ({
.map(url => ({
url,
author: p.author.npub,
content: prepareContent(p.content),
type:
url.endsWith(".mp4") || url.endsWith(".webm") ? "video" : "image",
type: url.endsWith('.mp4') || url.endsWith('.webm') ? 'video' : 'image',
timestamp: p.created_at,
noteId: nip19.noteEncode(p.id),
tags: p.tags
.filter((t: string[]) => t[0] === "t")
.map((t: string[]) => t[1].toLowerCase()),
tags: p.tags.filter((t: string[]) => t[0] === 't').map((t: string[]) => t[1].toLowerCase()),
}));
}),
"url"
'url'
);
console.log(images.current.length);
}, [posts]);
@ -160,11 +142,11 @@ const SlideShow = (settings: Settings) => {
const onKeyDown = (event: KeyboardEvent) => {
if (showSettings) return;
if (event.key === "g" || event.key === "G") {
setShowGrid((p) => !p);
if (event.key === 'g' || event.key === 'G') {
setShowGrid(p => !p);
}
if (event.key === "Escape") {
setShowSettings((s) => !s);
if (event.key === 'Escape') {
setShowSettings(s => !s);
}
/*
if (event.key === "f" || event.key === "F") {
@ -174,18 +156,17 @@ const SlideShow = (settings: Settings) => {
};
useEffect(() => {
window.addEventListener("keydown", onKeyDown);
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener('keydown', onKeyDown);
};
}, []);
const fullScreen = document.fullscreenElement !== null;
const showAdultContentWarning =
!settings.showNsfw && (
nfswTags.some((t) => settings.tags.includes(t)) ||
nsfwNPubs.some((p) => settings.npubs.includes(p)));
!settings.showNsfw &&
(nfswTags.some(t => settings.tags.includes(t)) || nsfwNPubs.some(p => settings.npubs.includes(p)));
if (showAdultContentWarning) {
return <AdultContentInfo></AdultContentInfo>;
@ -193,53 +174,29 @@ const SlideShow = (settings: Settings) => {
return (
<>
{showSettings && (
<Settings
onClose={() => setShowSettings(false)}
settings={settings}
></Settings>
)}
{showSettings && <Settings onClose={() => setShowSettings(false)} settings={settings}></Settings>}
<div className="controls">
<button
onClick={() => setShowGrid((g) => !g)}
title={showGrid ? "Play random slideshow (G)" : "view grid (G)"}
>
<button onClick={() => setShowGrid(g => !g)} title={showGrid ? 'Play random slideshow (G)' : 'view grid (G)'}>
{showGrid ? (
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 384 512"
>
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 384 512">
<path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 448 512"
>
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512">
<path d="M128 136c0-22.1-17.9-40-40-40L40 96C17.9 96 0 113.9 0 136l0 48c0 22.1 17.9 40 40 40H88c22.1 0 40-17.9 40-40l0-48zm0 192c0-22.1-17.9-40-40-40H40c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40H88c22.1 0 40-17.9 40-40V328zm32-192v48c0 22.1 17.9 40 40 40h48c22.1 0 40-17.9 40-40V136c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM288 328c0-22.1-17.9-40-40-40H200c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40h48c22.1 0 40-17.9 40-40V328zm32-192v48c0 22.1 17.9 40 40 40h48c22.1 0 40-17.9 40-40V136c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM448 328c0-22.1-17.9-40-40-40H360c-22.1 0-40 17.9-40 40v48c0 22.1 17.9 40 40 40h48c22.1 0 40-17.9 40-40V328z" />
</svg>
)}
</button>
<button onClick={() => setShowSettings((s) => !s)}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 512 512"
>
<button onClick={() => setShowSettings(s => !s)}>
<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>
{!fullScreen && (
<button
onClick={() =>
document?.getElementById("root")?.requestFullscreen()
}
>
<button onClick={() => document?.getElementById('root')?.requestFullscreen()}>
<IconFullScreen />
</button>
)}

View File

@ -1,24 +1,17 @@
import { useEffect } from "react";
import SlideImage from "./SlideImage";
import SlideVideo from "./SlideVideo";
import { useEffect } from 'react';
import SlideImage from './SlideImage';
import SlideVideo from './SlideVideo';
type SlideProps = {
url: string;
paused: boolean;
type: "image" | "video";
type: 'image' | 'video';
onAnimationEnded?: () => void;
animationDuration?: number;
noteId: string;
};
const Slide = ({
url,
paused,
type,
noteId,
onAnimationEnded,
animationDuration = 12,
}: SlideProps) => {
const Slide = ({ url, paused, type, noteId, onAnimationEnded, animationDuration = 12 }: SlideProps) => {
useEffect(() => {
const handle = setTimeout(() => {
onAnimationEnded && onAnimationEnded();
@ -28,20 +21,10 @@ const Slide = ({
};
}, []);
return type === "image" ? (
<SlideImage
url={url}
noteId={noteId}
paused={paused}
style={{ animationDuration: `${animationDuration}s` }}
/>
return type === 'image' ? (
<SlideImage url={url} noteId={noteId} paused={paused} style={{ animationDuration: `${animationDuration}s` }} />
) : (
<SlideVideo
url={url}
noteId={noteId}
paused={paused}
style={{ animationDuration: `${animationDuration}s` }}
/>
<SlideVideo url={url} noteId={noteId} paused={paused} style={{ animationDuration: `${animationDuration}s` }} />
);
};

View File

@ -1,4 +1,4 @@
import useImageLoaded from "../../utils/useImageLoaded";
import useImageLoaded from '../../utils/useImageLoaded';
type SlideImageProps = {
url: string;
@ -12,7 +12,7 @@ const SlideImage = ({ url, paused, style, noteId }: SlideImageProps) => {
return (
loaded && (
<div
className={`slide ${paused ? "paused" : ""}`}
className={`slide ${paused ? 'paused' : ''}`}
data-node-id={noteId}
style={{
backgroundImage: `url(${url})`,

View File

@ -7,7 +7,7 @@ type SlideVideoProps = {
const SlideVideo = ({ url, paused, style, noteId }: SlideVideoProps) => {
return (
<div className={`slide ${paused ? "paused" : ""}`} style={style}>
<div className={`slide ${paused ? 'paused' : ''}`} style={style}>
<video src={url} autoPlay loop muted playsInline data-node-id={noteId} />
</div>
);

View File

@ -1,13 +1,13 @@
import { useEffect, useRef, useState } from "react";
import AuthorProfile from "../AuthorProfile";
import Slide from "./Slide";
import { NostrImage, urlFix } from "../nostrImageDownload";
import { appName } from "../env";
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 { useEffect, useRef, useState } from 'react';
import AuthorProfile from '../AuthorProfile';
import Slide from './Slide';
import { NostrImage, urlFix } from '../nostrImageDownload';
import { appName } from '../env';
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';
type SlideViewProps = {
settings: Settings;
@ -25,13 +25,11 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
const [title, setTitle] = useState(appName);
const [activeNpub, setActiveNpub] = useState<string | undefined>(undefined);
const [slideShowStarted, setSlideShowStarted] = useState(false);
const [activeContent, setActiveContent] = useState<string | undefined>(
undefined
);
const [activeContent, setActiveContent] = useState<string | undefined>(undefined);
useEffect(() => {
if (settings.tags && settings.tags.length > 0) {
setTitle("#" + settings.tags.join(" #") + ` | ${appName}`);
setTitle('#' + settings.tags.join(' #') + ` | ${appName}`);
} else {
setTitle(`Random photos from popular hashtags | ${appName}`);
}
@ -81,7 +79,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
const animateImages = () => {
console.log(`animateImages ${images.length}`);
setActiveImages((activeImages) => {
setActiveImages(activeImages => {
const newActiveImages = [...activeImages];
console.log(`newActiveImages = ${JSON.stringify(newActiveImages)}`);
if (newActiveImages.length > 2) {
@ -94,7 +92,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
const randomImage = images[Math.floor(Math.random() * images.length)];
console.log(`randomImage = ${randomImage.url}`);
// TODO this creates potential duplicates when images are loaded from multiple relays
images = images.filter((i) => i !== randomImage);
images = images.filter(i => i !== randomImage);
history.current.push(randomImage);
newActiveImages.push(randomImage);
@ -105,34 +103,32 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
};
useEffect(() => {
console.log(
`slideShowStarted = ${slideShowStarted}, images = ${images.length}`
);
console.log(`slideShowStarted = ${slideShowStarted}, images = ${images.length}`);
// Make sure we have an image to start with but only trigger once
if (!slideShowStarted && images.length > 2) {
setSlideShowStarted(true);
console.log("******* queueNextImage");
console.log('******* queueNextImage');
queueNextImage(10);
}
}, [images]);
const onKeyDown = (event: KeyboardEvent) => {
// console.log(event);
if (event.key === "ArrowRight") {
if (event.key === 'ArrowRight') {
nextImage();
}
if (event.key === "ArrowLeft") {
if (event.key === 'ArrowLeft') {
previousImage();
}
if (event.key === "p" || event.key === " " || event.key === "P") {
setPaused((p) => !p);
if (event.key === 'p' || event.key === ' ' || event.key === 'P') {
setPaused(p => !p);
}
};
useEffect(() => {
document.body.addEventListener("keydown", onKeyDown);
document.body.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener('keydown', onKeyDown);
console.log(`cleaining timeout in useEffect[] destructor `);
clearTimeout(viewTimeoutHandle.current);
};
@ -161,34 +157,20 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
const activeProfile = activeNpub && getProfile(activeNpub);
useEffect(() => {
if (
settings.npubs.length>0 &&
activeProfile &&
(activeProfile.displayName || activeProfile.name)
) {
setTitle(
activeProfile.displayName || activeProfile.name + ` | ${appName}`
);
if (settings.npubs.length > 0 && activeProfile && (activeProfile.displayName || activeProfile.name)) {
setTitle(activeProfile.displayName || activeProfile.name + ` | ${appName}`);
}
}, [activeProfile]);
return (
<div
{...swipeHandlers}
onClick={() => setPaused((p) => !p)}
style={{ overflow: "hidden" }}
>
<div {...swipeHandlers} onClick={() => setPaused(p => !p)} style={{ overflow: 'hidden' }}>
<Helmet>
<title>{title}</title>
</Helmet>
{paused && (
<div className="centerSymbol">
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 320 512"
>
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 320 512">
<path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z" />
</svg>
</div>
@ -196,11 +178,7 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
{loading && (
<div className="centerSymbol spin">
<svg
xmlns="http://www.w3.org/2000/svg"
height="1em"
viewBox="0 0 512 512"
>
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512">
<path d="M256 96c38.4 0 73.7 13.5 101.3 36.1l-32.6 32.6c-4.6 4.6-5.9 11.5-3.5 17.4s8.3 9.9 14.8 9.9H448c8.8 0 16-7.2 16-16V64c0-6.5-3.9-12.3-9.9-14.8s-12.9-1.1-17.4 3.5l-34 34C363.4 52.6 312.1 32 256 32c-10.9 0-21.5 .8-32 2.3V99.2c10.3-2.1 21-3.2 32-3.2zM132.1 154.7l32.6 32.6c4.6 4.6 11.5 5.9 17.4 3.5s9.9-8.3 9.9-14.8V64c0-8.8-7.2-16-16-16H64c-6.5 0-12.3 3.9-14.8 9.9s-1.1 12.9 3.5 17.4l34 34C52.6 148.6 32 199.9 32 256c0 10.9 .8 21.5 2.3 32H99.2c-2.1-10.3-3.2-21-3.2-32c0-38.4 13.5-73.7 36.1-101.3zM477.7 224H412.8c2.1 10.3 3.2 21 3.2 32c0 38.4-13.5 73.7-36.1 101.3l-32.6-32.6c-4.6-4.6-11.5-5.9-17.4-3.5s-9.9 8.3-9.9 14.8V448c0 8.8 7.2 16 16 16H448c6.5 0 12.3-3.9 14.8-9.9s1.1-12.9-3.5-17.4l-34-34C459.4 363.4 480 312.1 480 256c0-10.9-.8-21.5-2.3-32zM256 416c-38.4 0-73.7-13.5-101.3-36.1l32.6-32.6c4.6-4.6 5.9-11.5 3.5-17.4s-8.3-9.9-14.8-9.9H64c-8.8 0-16 7.2-16 16l0 112c0 6.5 3.9 12.3 9.9 14.8s12.9 1.1 17.4-3.5l34-34C148.6 459.4 199.9 480 256 480c10.9 0 21.5-.8 32-2.3V412.8c-10.3 2.1-21 3.2-32 3.2z" />
</svg>
</div>
@ -213,20 +191,14 @@ const SlideView = ({ settings, images }: SlideViewProps) => {
)}
{activeProfile && (
<AuthorProfile
src={urlFix(activeProfile.image || "")}
src={urlFix(activeProfile.image || '')}
author={activeProfile.displayName || activeProfile.name}
npub={activeNpub}
></AuthorProfile>
)}
{activeImages.map((image) => (
<Slide
key={image.url}
noteId={image.noteId}
url={image.url}
paused={paused}
type={image.type}
/>
{activeImages.map(image => (
<Slide key={image.url} noteId={image.noteId} url={image.url} paused={paused} type={image.type} />
))}
</div>
);

View File

@ -1,91 +1,89 @@
import { nip19 } from "nostr-tools";
import { nip19 } from 'nostr-tools';
export const appName = "slidestr.net";
export const appName = 'slidestr.net';
export const defaultHashTags = [
"art",
"artstr",
"catstr",
"dogstr",
"nature",
"naturephotography",
"photography",
"photostr",
"streetphotography",
"tavelstr",
"gardening",
"gardenstr",
'art',
'artstr',
'catstr',
'dogstr',
'nature',
'naturephotography',
'photography',
'photostr',
'streetphotography',
'tavelstr',
'gardening',
'gardenstr',
];
export const nfswTags = [
"ass",
"blowjob",
"boobstr",
"buttstr",
"erostr",
"erotic",
"freethenipple",
"friskyfriday",
"humpday",
"kink",
"kinkstr",
"milf",
"naked",
"nakedart",
"nasstr",
"nsfw",
"nude",
"nudeart",
"orgasm",
"pornhub",
"pornstr",
"pussy",
"sex",
"suicidegirls",
"thighstr",
"tits",
"titstr",
'ass',
'blowjob',
'boobstr',
'buttstr',
'erostr',
'erotic',
'freethenipple',
'friskyfriday',
'humpday',
'kink',
'kinkstr',
'milf',
'naked',
'nakedart',
'nasstr',
'nsfw',
'nude',
'nudeart',
'orgasm',
'pornhub',
'pornstr',
'pussy',
'sex',
'suicidegirls',
'thighstr',
'tits',
'titstr',
];
export const nsfwNPubs = [
"npub12jedfuhk2wfr7syr38t2f55652khuyz9f88r63ftm0j2vudxq9sqq7677r", // Erikha
"npub13806pd9p833wkgyemeqddjzdksunlq9gszq4yjnhw4l57sjjhwlq6m79nj", // Orvalho
"npub13n6ednsew67xk7hgse670z7849q5h8su5rgydxtl4lq3r5cx4ecqsd9af4", // Everybody, Every Body
"npub177wu03dgx6zt9a9hdey079prfw5lvj5dhm20z4k96tf6um6zjk7qyzdumj", // Eoun
"npub19xwjw7f23nsmnsd0j72mvhrdswt4cp6urc5el2zuu8se3yfu87ess524je", // Gone Wild (NSFW)
"npub1af9lxfzeq5rxmu9zz7d85tn2ex8zvvlx0duqcemcdhkz9cvlt29st3rcgd", // Happy Nut
"npub1e4n8nah09he25slv00dz3kav3jsu5jvp83aya234ejumcmu2xseqwrp6pl", // Svenno(NSFW)
"npub1femd0mrawr0jmtjr2jwa2nm90haxrpglzdt6tt0djrsav39e53asf74aer", // FemDom Raw
"npub1ga79p6qsjh0xd343q3du2unf2gl6gk0rde36c06mafxkrssmnnesxyzcss", // Orange Incest 🔞
"npub1grssdyrmdgy5gw5umg50u6rrl9nk738lw4qg2thpcqqaf3lypkqsxt7lhg", // Shades of Red
"npub1j0xvl8l2s4w25vcavf3jv6fgyakyc0hplxc9we8hc8mja0ct7epss0qlkj", // Thicc Pics (NSFW)
"npub1j0y6f9gl9w39ggarr9x76lyh2swv7mpgddguv49mhmzqlz8tm69qcwpl55", // NeoMobius
"npub1j2u3lfkhl95e6qwswr32hx6h36arlw8p2cl6hy0wgnmxmekrhx8qx93uvh", // Ay Papi
"npub1jge7z2kpmpdra6g58vg95uznve8ctcenmlyp9ntr3kjymscyuqpqty2cdh", // Storm
"npub1jjtzhxzu8dlf7yn480sz67tesnfl7gpzfpkgpez05d2z9y3lya5sxvky0y", // Selfie Girls
"npub1jp9v034z3a26cp5hajwyuzl0hety5akdpwdnjaqgfd7pm2ts4dwsc29va8", // curatedbliss
"npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an", // Aeontropy
"npub1klxseqx4et3grzgvajtlm47tz7tqmxygwj49kx3frsuls9cf8lhqhhhr5q", // Riley_R_Fan
"npub1kul999wnt8gwa6l2vyuewhnmmp25gq7dly9zmgsw52x8csmqjgts7278rx", // 𝓟𝓮𝓽𝓲𝓽𝓮 𝓟𝓻𝓲𝓷𝓬𝓮𝓼𝓼
"npub1m5fdz9gqa2qeudpy47zllmv9gqe3zzj44dkt9lh2kes3mlex7e6se348vy", // Marble Sculture
"npub1mgusda7ujnyuhhudwkyrp763k4dd9xspktekl0tg5v0j76yph8ssyrfdpm", // anisyia
"npub1nme4074q6yrqexdn5z625vhvv9j9e2qwwfcgdyg2utffhvdgrxfqn5ztgm", // Ay Papi
"npub1pl0qa9x3n8wt55em0x3zwuy02rtl5t3jsretr0egjqgkx2f6jztqt0xwew", // nude
"npub1rv08kght99a7xwckm0qpmzw09m5gwppequgqd8lwu74eakgaavwsp5cjtw", // CuratedNSFW
"npub1suddec4n2jv50pgn9eea35r4k83ahr4mcj0zv2uec36w6jeuwagq82xjgl", // quiet.enjoyer
"npub1t252vm7u5qmfwv3k70g6rl2ue7ctvtvrnd60vy8jh5suglv8pw2snyyzfq", // 20th Century Foxes (NSFW)
"npub1thsprukxnc8rxqggnesqp2wg2temhaadzhhg7n4pttpveyqedlwsqgge9q", // Harmony. Corrupted.
"npub1ulafm4d3n7ukl7yzg4hfnhfjut74nym5p83e3d67l3j62yc6ysqqrancw2", // naked
"npub1ve4ztpqvlgu3v6hgrvc4lrdl2ernue7lq2h8tcgaksrkxlm7gnsqkjmz4e", // bluntkaraoke
"npub1wmsn8fch7kwt987jcdx06uuapn6pwzau59pvy0ql5d3xlmnxa2csj3c5p4", // StefsPicks
"npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv", // Erandis Vol
"npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0", // sexy-models
"npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq", // Welcome To The Jungle
"npub1csk2wg33ee9kutyps4nmevyv3putfegj7yd0emsp44ph32wvmamqs7uyan", // Lilura
'npub12jedfuhk2wfr7syr38t2f55652khuyz9f88r63ftm0j2vudxq9sqq7677r', // Erikha
'npub13806pd9p833wkgyemeqddjzdksunlq9gszq4yjnhw4l57sjjhwlq6m79nj', // Orvalho
'npub13n6ednsew67xk7hgse670z7849q5h8su5rgydxtl4lq3r5cx4ecqsd9af4', // Everybody, Every Body
'npub177wu03dgx6zt9a9hdey079prfw5lvj5dhm20z4k96tf6um6zjk7qyzdumj', // Eoun
'npub19xwjw7f23nsmnsd0j72mvhrdswt4cp6urc5el2zuu8se3yfu87ess524je', // Gone Wild (NSFW)
'npub1af9lxfzeq5rxmu9zz7d85tn2ex8zvvlx0duqcemcdhkz9cvlt29st3rcgd', // Happy Nut
'npub1e4n8nah09he25slv00dz3kav3jsu5jvp83aya234ejumcmu2xseqwrp6pl', // Svenno(NSFW)
'npub1femd0mrawr0jmtjr2jwa2nm90haxrpglzdt6tt0djrsav39e53asf74aer', // FemDom Raw
'npub1ga79p6qsjh0xd343q3du2unf2gl6gk0rde36c06mafxkrssmnnesxyzcss', // Orange Incest 🔞
'npub1grssdyrmdgy5gw5umg50u6rrl9nk738lw4qg2thpcqqaf3lypkqsxt7lhg', // Shades of Red
'npub1j0xvl8l2s4w25vcavf3jv6fgyakyc0hplxc9we8hc8mja0ct7epss0qlkj', // Thicc Pics (NSFW)
'npub1j0y6f9gl9w39ggarr9x76lyh2swv7mpgddguv49mhmzqlz8tm69qcwpl55', // NeoMobius
'npub1j2u3lfkhl95e6qwswr32hx6h36arlw8p2cl6hy0wgnmxmekrhx8qx93uvh', // Ay Papi
'npub1jge7z2kpmpdra6g58vg95uznve8ctcenmlyp9ntr3kjymscyuqpqty2cdh', // Storm
'npub1jjtzhxzu8dlf7yn480sz67tesnfl7gpzfpkgpez05d2z9y3lya5sxvky0y', // Selfie Girls
'npub1jp9v034z3a26cp5hajwyuzl0hety5akdpwdnjaqgfd7pm2ts4dwsc29va8', // curatedbliss
'npub1kade5vf37snr4hv5hgstav6j5ygry6z09kkq0flp47p8cmeuz5zs7zz2an', // Aeontropy
'npub1klxseqx4et3grzgvajtlm47tz7tqmxygwj49kx3frsuls9cf8lhqhhhr5q', // Riley_R_Fan
'npub1kul999wnt8gwa6l2vyuewhnmmp25gq7dly9zmgsw52x8csmqjgts7278rx', // 𝓟𝓮𝓽𝓲𝓽𝓮 𝓟𝓻𝓲𝓷𝓬𝓮𝓼𝓼
'npub1m5fdz9gqa2qeudpy47zllmv9gqe3zzj44dkt9lh2kes3mlex7e6se348vy', // Marble Sculture
'npub1mgusda7ujnyuhhudwkyrp763k4dd9xspktekl0tg5v0j76yph8ssyrfdpm', // anisyia
'npub1nme4074q6yrqexdn5z625vhvv9j9e2qwwfcgdyg2utffhvdgrxfqn5ztgm', // Ay Papi
'npub1pl0qa9x3n8wt55em0x3zwuy02rtl5t3jsretr0egjqgkx2f6jztqt0xwew', // nude
'npub1rv08kght99a7xwckm0qpmzw09m5gwppequgqd8lwu74eakgaavwsp5cjtw', // CuratedNSFW
'npub1suddec4n2jv50pgn9eea35r4k83ahr4mcj0zv2uec36w6jeuwagq82xjgl', // quiet.enjoyer
'npub1t252vm7u5qmfwv3k70g6rl2ue7ctvtvrnd60vy8jh5suglv8pw2snyyzfq', // 20th Century Foxes (NSFW)
'npub1thsprukxnc8rxqggnesqp2wg2temhaadzhhg7n4pttpveyqedlwsqgge9q', // Harmony. Corrupted.
'npub1ulafm4d3n7ukl7yzg4hfnhfjut74nym5p83e3d67l3j62yc6ysqqrancw2', // naked
'npub1ve4ztpqvlgu3v6hgrvc4lrdl2ernue7lq2h8tcgaksrkxlm7gnsqkjmz4e', // bluntkaraoke
'npub1wmsn8fch7kwt987jcdx06uuapn6pwzau59pvy0ql5d3xlmnxa2csj3c5p4', // StefsPicks
'npub1xfu7047thly6aghl79z97kckkvwfvtcx88n6wq7c2tlng484d8xqv0kuvv', // Erandis Vol
'npub1y77j6jm5hw34xl5m85aumltv88arh2s7q383allkpfe4muarzc5qzfgru0', // sexy-models
'npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq', // Welcome To The Jungle
'npub1csk2wg33ee9kutyps4nmevyv3putfegj7yd0emsp44ph32wvmamqs7uyan', // Lilura
];
export const nsfwPubKeys = nsfwNPubs.map((npub) =>
(nip19.decode(npub).data as string).toLowerCase()
);
export const nsfwPubKeys = nsfwNPubs.map(npub => (nip19.decode(npub).data as string).toLowerCase());
export const spamAccounts = [];

View File

@ -1,6 +1,6 @@
import { NDKFilter } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
import { nfswTags } from "./env";
import { NDKFilter } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { nfswTags } from './env';
export type NostrImage = {
url: string;
@ -9,7 +9,7 @@ export type NostrImage = {
content?: string;
timestamp: number;
noteId: string;
type: "image" | "video";
type: 'image' | 'video';
};
export const buildFilter = (tags: string[], npubs: string[]) => {
@ -18,55 +18,47 @@ export const buildFilter = (tags: string[], npubs: string[]) => {
};
if (npubs && npubs.length > 0) {
filter.authors = npubs.map((p) => nip19.decode(p).data as string);
filter.authors = npubs.map(p => nip19.decode(p).data as string);
} else {
if (tags && tags.length > 0) {
filter["#t"] = tags;
}
filter['#t'] = tags;
}
}
console.log("filter", filter);
console.log('filter', filter);
return filter;
};
export const prepareContent = (content: string) => {
return content
.replace(/https?:\/\/[^\s]+/g, "") // remove all urls
.replace(/#[^\s]+/g, ""); // remove all tags
.replace(/https?:\/\/[^\s]+/g, '') // remove all urls
.replace(/#[^\s]+/g, ''); // remove all tags
};
export const urlFix = (url: string) => {
// dont use cdn for mp4/webm
if (url == undefined || url.endsWith(".mp4") || url.endsWith(".webm")) return url;
if (url == undefined || url.endsWith('.mp4') || url.endsWith('.webm')) return url;
// use cdn for nostr.build
return url.replace(/https?:\/\/nostr.build/, "https://cdn.nostr.build");
return url.replace(/https?:\/\/nostr.build/, 'https://cdn.nostr.build');
};
export const extractImageUrls = (text: string): string[] => {
const urlRegex = /(https?:\/\/[^\s]+)/g;
return (text.match(urlRegex) || []).map((u) => urlFix(u));
return (text.match(urlRegex) || []).map(u => urlFix(u));
};
export const isReply = (event: any) => {
// ["e", "aab5a68f29d76a04ad79fe7e489087b802ee0f946689d73b0e15931dd40a7af3", "", "reply"]
return (
event.tags.filter((t: string[]) => t[0] === "e" && t[3] === "reply")
.length > 0
);
return event.tags.filter((t: string[]) => t[0] === 'e' && t[3] === 'reply').length > 0;
};
export const hasContentWarning = (event: any) => {
// ["content-warning", "NSFW: implied nudity"]
return (
event.tags.filter((t: string[]) => t[0] === "content-warning").length > 0
);
return event.tags.filter((t: string[]) => t[0] === 'content-warning').length > 0;
};
export const hasNsfwTag = (event: any) => {
// ["e", "aab5a68f29d76a04ad79fe7e489087b802ee0f946689d73b0e15931dd40a7af3", "", "reply"]
return (
event.tags.filter((t: string[]) => t[0] === "t" && nfswTags.includes(t[1]))
.length > 0
);
return event.tags.filter((t: string[]) => t[0] === 't' && nfswTags.includes(t[1])).length > 0;
};

View File

@ -1,5 +1,5 @@
:root {
font-family: "Outfit", sans-serif;
font-family: 'Outfit', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
@ -64,8 +64,7 @@ button {
}
}
input[type="checkbox"] {
input[type='checkbox'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@ -76,8 +75,8 @@ input[type="checkbox"] {
background-color: transparent;
}
input[type="checkbox"]:after {
content: " ";
input[type='checkbox']:after {
content: ' ';
position: relative;
left: 40%;
top: 20%;
@ -89,7 +88,7 @@ input[type="checkbox"]:after {
display: none;
}
input[type="checkbox"]:checked:after {
input[type='checkbox']:checked:after {
display: block;
}
@ -102,7 +101,6 @@ div.paper {
align-items: center;
}
.btn {
border: none;
outline: none;
@ -121,13 +119,15 @@ div.paper {
.btn-border {
border: 1px solid transparent;
color: inherit;
background: linear-gradient(black, black) padding-box,
background:
linear-gradient(black, black) padding-box,
linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
transition: 0.3s;
}
.btn-border:hover {
background: linear-gradient(black, black) padding-box,
background:
linear-gradient(black, black) padding-box,
linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
}
@ -155,4 +155,4 @@ div.paper {
align-items: center;
justify-content: center;
gap: 8px;
}
}

View File

@ -1,38 +1,38 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { NDKProvider } from "@nostr-dev-kit/ndk-react";
import App from "./App";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import React from 'react';
import ReactDOM from 'react-dom/client';
import { NDKProvider } from '@nostr-dev-kit/ndk-react';
import App from './App';
import './index.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: "/",
path: '/',
element: <App />,
},
{
path: "global",
path: 'global',
element: <App />,
},
{
path: "tags/:tags",
path: 'tags/:tags',
element: <App />,
},
{
path: "profile/:npub",
path: 'profile/:npub',
element: <App />,
},
{
path: "p/:npub",
path: 'p/:npub',
element: <App />,
},
{
path: "/:npub",
path: '/:npub',
element: <App />,
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<NDKProvider>
<RouterProvider router={router} />
</NDKProvider>

View File

@ -1,12 +1,12 @@
import { DependencyList, EffectCallback, useEffect } from "react";
import { DependencyList, EffectCallback, useEffect } from 'react';
const useDebouncedEffect = (effect: EffectCallback, deps?: DependencyList, delay?: number) => {
useEffect(() => {
const handler = setTimeout(() => effect(), delay);
useEffect(() => {
const handler = setTimeout(() => effect(), delay);
return () => clearTimeout(handler);
return () => clearTimeout(handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...(deps || []), delay]);
}
}, [...(deps || []), delay]);
};
export default useDebouncedEffect;
export default useDebouncedEffect;

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react';
declare global {
interface Window {
@ -10,9 +10,7 @@ const useDisclaimerState = () => {
const [disclaimerAccepted, setDisclaimerAccepted] = useState(false);
useEffect(() => {
const disclaimerAcceptedPreviously = JSON.parse(
localStorage.getItem("disclaimerAccepted") as string
);
const disclaimerAcceptedPreviously = JSON.parse(localStorage.getItem('disclaimerAccepted') as string);
if (disclaimerAcceptedPreviously === true) {
setDisclaimerAccepted(true);
}
@ -22,7 +20,7 @@ const useDisclaimerState = () => {
disclaimerAccepted,
setDisclaimerAccepted: (accepted: boolean) => {
setDisclaimerAccepted(accepted);
localStorage.setItem("disclaimerAccepted", JSON.stringify(accepted));
localStorage.setItem('disclaimerAccepted', JSON.stringify(accepted));
},
};
};

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState } from 'react';
const useImageLoaded = (src?: string) => {
const [loaded, setLoaded] = useState(false);