feat: Moved global and nsfw section to home page

This commit is contained in:
florian 2024-03-05 14:02:42 +00:00
parent f7b3dad4ac
commit 252612e7df
16 changed files with 154 additions and 54 deletions

View File

@ -12,36 +12,36 @@
"analyze": "vite-bundle-visualizer"
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.4.2",
"@nostr-dev-kit/ndk-cache-dexie": "^2.2.6",
"@tanstack/react-query": "^5.22.2",
"@nostr-dev-kit/ndk": "^2.5.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.2.7",
"@tanstack/react-query": "^5.24.8",
"bech32": "^2.0.0",
"jotai": "^2.6.5",
"jotai": "^2.7.0",
"lodash": "^4.17.21",
"nostr-tools": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-lazy-load": "^4.0.1",
"react-router-dom": "^6.22.1",
"react-router-dom": "^6.22.2",
"react-swipeable": "^7.0.1"
},
"devDependencies": {
"@types/lodash": "^4.14.202",
"@types/react": "^18.2.57",
"@types/react": "^18.2.63",
"@types/react-dom": "^18.2.19",
"@types/react-helmet": "^6.1.11",
"@types/react-swipeable": "^5.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-react": "^4.2.1",
"@webbtc/webln-types": "^3.0.0",
"eslint": "^8.56.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"prettier": "^3.2.5",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite": "^5.1.5",
"vite-bundle-visualizer": "^1.0.1"
}
}

View File

@ -48,7 +48,6 @@ h1 {
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;

View File

@ -30,7 +30,6 @@ const GridView = ({ settings, images, currentImage, setCurrentImage, setViewMode
};
const onKeyDown = (event: KeyboardEvent) => {
console.log(event);
if (event.key === 'ArrowRight') {
showNextImage();

View File

@ -4,9 +4,11 @@ import './Home.css';
import { useGlobalState } from '../../utils/globalState';
import usePeopleLists from '../../utils/useLists';
import { createImgProxyUrl } from '../nostrImageDownload';
import { useState } from 'react';
const Home = () => {
const { nav, currentSettings } = useNav();
const [showAdult, setShowAdult] = useState(currentSettings.showAdult || false);
const [state] = useGlobalState();
const topicKeys = Object.keys(topics);
const lists = usePeopleLists(state.userNPub);
@ -18,17 +20,34 @@ const Home = () => {
<div className="topics">
{topicKeys.map(tk => (
<div
key={tk}
className="topic"
style={{
backgroundImage: `linear-gradient(170deg, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0) 100%), url(${createImgProxyUrl('https://slidestr.net/images/'+tk+'.jpg', 600, -1)})`,
backgroundImage: `linear-gradient(170deg, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0) 100%), url(${createImgProxyUrl('https://slidestr.net/images/' + tk + '.jpg', 600, -1)})`,
}}
onClick={() =>
nav({ ...currentSettings, topic: tk, npubs: [], tags: [], list: undefined, follows: false })
nav({ ...currentSettings, topic: tk, npubs: [], tags: [], list: undefined, follows: false, showAdult })
}
>
<div className="topic-title">{topics[tk].name || tk}</div>
</div>
))}
<div
className="topic"
onClick={() =>
nav({
tags: [],
npubs: [],
showReplies: false,
showReposts: false,
follows: false,
showAdult,
})
}
>
<div className="topic-title">Global</div>
<div>All content posted on nostr. Use with care!</div>
</div>
</div>
{state.userNPub && (
<>
@ -47,6 +66,7 @@ const Home = () => {
follows: true,
showReplies: false,
showReposts: true,
showAdult,
})
}
>
@ -60,18 +80,37 @@ const Home = () => {
<div className="topics">
{lists.map(l => (
<div
key={l.nevent}
className="topic"
style={{}}
onClick={() =>
nav({ ...currentSettings, tags: [], npubs: [], list: l.nevent, topic: undefined, follows: false })
nav({
...currentSettings,
tags: [],
npubs: [],
list: l.nevent,
topic: undefined,
follows: false,
showAdult,
})
}
>
<div className="topic-title">{l.name}</div>
{l.description && <div>{l.description}</div>}
</div>
))}
</div>
</>
)}
<div className={`content-warning ${showAdult ? 'active' : ''}`} style={{ marginTop: '5em' }}>
<div>
<input name="adult" type="checkbox" checked={showAdult} onChange={() => setShowAdult(sa => !sa)} />
</div>
<label htmlFor="adult" onClick={() => setShowAdult(sa => !sa)} style={{ userSelect: 'none' }}>
<div className="warning">NSFW / adult content</div>
Allow adult content to be shown and ignore content warnings.
</label>
</div>
<div className="footer">
Made with 💜 by{' '}
<a href="https://njump.me/npub1klr0dy2ul2dx9llk58czvpx73rprcmrvd5dc7ck8esg8f8es06qs427gxc" target="blank">

View File

@ -55,16 +55,17 @@
flex-direction: row;
gap: 8px;
}
.login-dialog .login-address button {
.login-dialog button,
.login-dialog button:visited,
.login-dialog button:active {
background-color: rgb(99, 19, 173);
}
.login-dialog .login-extension {
align-self: center;
color: #fff;
height: 3em;
}
.login-dialog .login-extension button {
height: 3em;
background-color: rgb(99, 19, 173);
.login-dialog .login-extension {
align-self: center;
}
.login-dialog .close-button {

View File

@ -82,7 +82,7 @@ const MasonryImage = ({ image, onClick, index }: MasonryImageProps) => {
)}
</a>
{(showAuthor || description || showTags.length > 0) && (
<div style={{ display: 'block', lineHeight: '1.4em', paddingBottom: '.5em', paddingTop: '.5em', position: 'relative' }}>
<div className="info-section">
<div className="time">{image.timestamp && timeDifference(now, image.timestamp)}</div>
{showAuthor && (
<div style={{ paddingBottom: '.25em' }}>

View File

@ -64,6 +64,14 @@
outline: 1px solid #fff;
}
.mason-imagegrid .info-section {
display: block;
line-height: 1.4em;
padding-bottom: 0.5em;
padding-top: 0.5em;
position: relative;
}
.profile-header {
padding: 1.2em;
padding-bottom: 0.6em;
@ -95,9 +103,9 @@
.mason-imagegrid .time {
position: absolute;
right: 0px;
top:0px;
top: 0px;
color: #666;
padding-top: .5em;
padding-top: 0.5em;
background-color: #111;
padding-left: .4em;
}
padding-left: 0.4em;
}

View File

@ -81,15 +81,25 @@
.content-warning {
padding: 16px;
border-radius: 16px;
border: 1px solid #ff563f;
border: 1px solid #444;
display: flex;
gap: 12px;
}
.warning {
.content-warning.active {
border: 1px solid #ff563f;
}
.content-warning.active .warning {
color: #ff563f;
}
.content-warning .warning {
color: #444;
font-weight: 500;
font-size: 1.2rem;
cursor: pointer;
}
.settings .replies,

View File

@ -182,7 +182,7 @@ const SettingsDialog = ({ onClose, setViewMode }: SettingsProps) => {
</div>
</>
)}
<div className="content-warning">
<div className={`content-warning ${showAdult ? 'active' : ''}`}>
<div>
<input name="adult" type="checkbox" checked={showAdult} onChange={e => setShowAdult(e.target.checked)} />
</div>

View File

@ -91,19 +91,34 @@ const SlideShow = () => {
const listAuthors = useAuthorsFromList(settings.list);
const [contacts] = useAtom(followsAtom);
const authorsToQuery = settings.follows
? contacts?.tags.filter(t => t[0] === 'p').map(t => t[1]) || []
: listAuthors && listAuthors.length > 0
? listAuthors
: settings.npubs.map(p => nip19.decode(p).data as string);
const filter = useMemo(() => {
const authorsToQuery = settings.follows
? contacts?.tags.filter(t => t[0] === 'p').map(t => t[1]) || []
: listAuthors && listAuthors.length > 0
? listAuthors
: settings.npubs.map(p => nip19.decode(p).data as string);
const filterTags = settings.topic ? topics[settings.topic].tags : settings.tags;
const filterTags = settings.topic ? topics[settings.topic].tags : settings.tags;
const { events } = useEvents(buildFilter(filterTags, authorsToQuery, settings.showReposts), {
return buildFilter(filterTags, authorsToQuery, settings.showReposts);
}, [
contacts?.tags,
listAuthors,
settings.follows,
settings.npubs,
settings.showReposts,
settings.tags,
settings.topic,
]);
const { events } = useEvents(filter, {
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
// when seeing global, close stream because of too many updates.
closeOnEose: settings.npubs.length == 0 && settings.tags.length == 0
});
useEffect(() => {
// console.log('set posts');
setPosts(
events
.filter(

View File

@ -47,7 +47,19 @@ export const topics: { [key: string]: Topic } = {
},
lifestyle: {
name: 'Lifestyle',
tags: ['fashion', 'flowerstr', 'foodstr', 'style', 'weedstr', 'travel', 'travelstr', 'happy', 'life', 'love'],
tags: [
'fashion',
'flowerstr',
'foodstr',
'style',
'weedstr',
'travel',
'travelstr',
'happy',
'life',
'love',
'hiking',
],
},
gardenandfarm: { name: 'Gardening und Farming', tags: ['gardening', 'gardenstr', 'nature', 'farming', 'farmstr'] },
};
@ -132,6 +144,7 @@ export const adultContentTags = [
'nakedart',
'nasstr',
'nodestr',
'naughty',
'nsfw',
'nude',
'nudeart',
@ -240,6 +253,7 @@ export const adultNPubs = [
'npub1ylrnf0xfp9wsmqthxlqjqyqj9yy27pnchjwjq93v3mq66ts7ftjs6x7dcq', // Welcome To The Jungle
'npub1z0xv9t5w6evrcg860kmgqq5tfj55mz84ta40uszjnfp9uhw2clkq63yrak', // ???
'npub1f3n7hq0a6vyfsjrv9vfdwtasa0g98ve96he68rxsvq9x6cl8tvxqmv6ca4', // Lady Sex (nude anime)
'npub1ylq5s3xsdmzgzvgzll6ghcs3qa8a9ajl955hj4tcpmyruvjsl8nq5wqhd8', // Dnera
'npub1t07mr7m65lg3ecr5eapu6qe4ayt2wgjpqjs8x58m5kx2r2cutsyqyzzzs9', // NOT NSFW but spammy ai pictures
'npub1curnt7jtq8mhl9fcswnwvuvc9ccm6lvsdv4kzydx75v92kldrvdqh7sq09', // NOT NSFW but spammy ai pictures

View File

@ -1,6 +1,7 @@
import { NDKEvent, NDKFilter, NDKTag } from '@nostr-dev-kit/ndk';
import { adultContentTags, adultPublicKeys, mixedAdultNPubs } from './env';
import uniq from 'lodash/uniq';
import { unixNow } from '../ngine/time';
export type Post = {
event: NDKEvent;
@ -23,7 +24,7 @@ export type NostrImage = {
export const buildFilter = (tags: string[], authors: string[], withReposts = false) => {
const filter: NDKFilter = {
kinds: [1, 1063] as number[],
limit: authors.length > 0 ? 1000 : 500,
limit: authors.length > 0 ? 1000 : tags.length > 0 ? 500 : 500,
};
if (withReposts) {
@ -32,10 +33,10 @@ export const buildFilter = (tags: string[], authors: string[], withReposts = fal
if (authors && authors.length > 0) {
filter.authors = authors;
} else if (tags && tags.length > 0) {
filter['#t'] = tags;
} else {
if (tags && tags.length > 0) {
filter['#t'] = tags;
}
filter.since = unixNow() - 60 * 60 * 24; // 24h
}
// console.log('filter', filter);
@ -67,6 +68,8 @@ export const urlFix = (url: string) => {
};
export const extractImageUrls = (text: string): string[] => {
if (text == undefined) return [];
const urlRegex = /(https?:\/\/[^\s]+)/g;
const matchedUrls = (text.match(urlRegex) || []).map(u => urlFix(u));
return uniq(matchedUrls);

View File

@ -15,16 +15,18 @@ export default function useEvents(filter: NDKFilter | NDKFilter[], opts?: Subscr
const [eose, setEose] = useState(false);
const [events, setEvents] = useState<NDKEvent[]>([]);
const id = useMemo(() => {
console.warn('new ID!!!');
return hashSha256(filter);
}, [filter]);
useEffect(() => {
if (filter && !opts?.disable) {
console.log('useEvents: new Subscription');
// console.log('useEvents: new Subscription', filter, opts);
setEvents([]);
const relaySet = relays?.length ?? 0 > 0 ? NDKRelaySet.fromRelayUrls(relays as string[], ndk) : undefined;
const sub = ndk.subscribe(filter, opts, relaySet);
sub.on('event', (ev: NDKEvent) => {
// console.log('new event ');
setEvents(evs => {
const newEvents = evs.concat([ev]).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
return uniqBy(newEvents, (e: NDKEvent) => e.tagId());

View File

@ -1,7 +1,8 @@
import { useMemo } from 'react';
import useEvent from '../ngine/hooks/useEvent';
import { nip19 } from 'nostr-tools';
const useAuthorsFromList = (listAddr?: string) => {
const useAuthorsFromList = (listAddr?: string): string[] => {
const validListAttr = listAddr?.indexOf('naddr') == 0;
const addr = listAddr ? (nip19.decode(listAddr).data as nip19.AddressPointer) : undefined;
const addrIsDefined = addr && addr.pubkey && addr.identifier;
@ -13,13 +14,17 @@ const useAuthorsFromList = (listAddr?: string) => {
{ kinds: [30000], authors: authorFilter, '#d': identFilter },
{ disable: !validListAttr && !addrIsDefined }
);
const authors: string[] =
(validListAttr &&
listEvent
?.getMatchingTags('p')
.map(t => t[1])
.flat()) ||
[];
const authors: string[] = useMemo(() => {
return (
(validListAttr &&
listEvent
?.getMatchingTags('p')
.map(t => t[1])
.flat()) ||
[]
);
}, [listEvent, validListAttr]);
return authors;
};

View File

@ -16,10 +16,14 @@ const usePeopleLists = (npub?: string) => {
return eventsWithName.map(e => {
const nameTag = e.getMatchingTags('d').slice(0, 1).flat();
const name = nameTag ? nameTag[1] : 'unknown';
const titleTag = e.getMatchingTags('title').slice(0, 1).flat();
const descriptionTag = e.getMatchingTags('description').slice(0, 1).flat();
const name = titleTag.length>0 ? titleTag[1] : nameTag ? nameTag[1] : 'unknown';
const description = descriptionTag.length > 0 && descriptionTag[1];
const people = e.tags.filter(t => t[0] === 'p')?.map(t => t[1]);
return { id: e.id, nevent: e.encode(), name, people };
return { id: e.id, nevent: e.encode(), name, people, description };
});
}, [events]);

View File

@ -52,6 +52,7 @@ const useNav = () => {
}
const postfix = searchParams.length > 0 ? `?${searchParams.join('&')}` : '';
console.log(settings);
if (settings.topic) {
navigate(`/topic/${settings.topic}${postfix}`);
} else if (settings.follows) {
@ -63,7 +64,7 @@ const useNav = () => {
} else if (validNpubs.length == 1) {
navigate(`/p/${validNpubs[0]}${postfix}`);
} else {
navigate(`/${postfix}`);
navigate(`/global${postfix}`);
}
};