From 9b5a105f656857959bf95c291ecf8b859a81f5a7 Mon Sep 17 00:00:00 2001 From: MSG <59928086+MSghais@users.noreply.github.com> Date: Tue, 28 May 2024 14:04:51 +0200 Subject: [PATCH] Feat comment Nostr (#123) * add comment send & fetch + fix send note + fix hooks query tanstack with params * fix eslint and unused var * run yarn format * import styles native * only use pool for multi relays push + delete var * run yarn lint:fix --- JoyboyCommunity/README.md | 160 ++++++++------ JoyboyCommunity/src/context/NostrContext.tsx | 23 ++- JoyboyCommunity/src/hooks/useNostr.tsx | 62 ++++-- .../src/shared/components/Comments.tsx | 195 +++++++++++------- .../src/shared/components/PostDetails.tsx | 2 +- .../src/shared/components/styled.ts | 18 ++ JoyboyCommunity/src/types/index.ts | 8 +- JoyboyCommunity/src/utils/relay.ts | 5 + JoyboyCommunity/tsconfig.json | 10 +- README.md | 1 - 10 files changed, 313 insertions(+), 171 deletions(-) create mode 100644 JoyboyCommunity/src/shared/components/styled.ts diff --git a/JoyboyCommunity/README.md b/JoyboyCommunity/README.md index e4f96132..ae48a3d4 100644 --- a/JoyboyCommunity/README.md +++ b/JoyboyCommunity/README.md @@ -1,61 +1,99 @@ -## Mobile - -Joyboy Mobile folder in React-native with Expo. -Pick a good first issue with the mobile labels, and let's contribute and keep building cool things in Open-source. - -### Test - -To test the project, run: - -```bash -cd JoyboyCommunity -yarn install -yarn start -``` - -Select Expo web, Android or IOS. You can scan it with Expo GO on your phone. - -### Screens of the mobile app -Recent implementation PoC on React Native. -Here is what we have on the mobile app on the first days, contributions welcome! -Check the issues with the "mobile" labels. - -Done: -- Create or import Nostr account -- Feed for notes -- See user page with notes -- Create note on Nostr - -WIP: -- Read Nostr tags content -- View user page details: followers and follows, replies, likes, repost with all details of the users. -- Note interactions: Replies, Likes, Report, Tips -- My profile: Update my profile -- Feed: Search, Trending fees, For you, Discover -- DM with private message: NIP-17 -- Public chat: NIP-28 - -[UI/UX proposal for video discussions](https://github.com/keep-starknet-strange/joyboy/discussions/48#discussion-6683225) - -[UI video discussions](https://t.me/JoyboyStarknet/206/397) - - -Home page: - -onboard - -Create Nostr account - -create account - -Feed by default: - -feed default - -User feed with notes: - -user-profile-details - -My profile page: WIP - -my-profile +## Mobile + +Joyboy Mobile folder in React-native with Expo. +Pick a good first issue with the mobile labels, and let's contribute and keep building cool things in Open-source. + +### Test + +To test the project, run: + +```bash +cd JoyboyCommunity +yarn install +yarn start +``` + +Select Expo web, Android or IOS. You can scan it with Expo GO on your phone. + +### Screens of the mobile app +Recent implementation PoC on React Native. +Here is what we have on the mobile app on the first days, contributions welcome! +Check the issues with the "mobile" labels. + +Done: +- Create or import Nostr account +- Feed for notes +- See user page with notes +- Create note on Nostr + +WIP: +- Read Nostr tags content +- View user page details: followers and follows, replies, likes, repost with all details of the users. +- Note interactions: Replies, Likes, Report, Tips +- My profile: Update my profile +- Feed: Search, Trending fees, For you, Discover +- DM with private message: NIP-17 +- Public chat: NIP-28 + +[UI/UX proposal for video discussions](https://github.com/keep-starknet-strange/joyboy/discussions/48#discussion-6683225) + +[UI video discussions](https://t.me/JoyboyStarknet/206/397) + + +Home page: + +onboard + +Create Nostr account + +create account + +Feed by default: + +feed default + +User feed with notes: + +user-profile-details + +My profile page: WIP + +my-profile + + +## How Nostr work + +- [Nostr Implementation Possibilities](https://github.com/nostr-protocol/nips) +- [JoinStr: Decentralized CoinJoin Implementation Using Nostr](https://www.nobsbitcoin.com/joinstr-decentralized-coinjoin-implementation-using-nostr/) + +### NIP-1 +Nostr event are described like this on NIP-1: + +```ts +export interface Event { + kind: number; // 1 for note, 0 for profile, + tags: string[][]; // NIP-10 for comment root and reply + // + content: string; + created_at: number; + pubkey: string; + id: string; + sig: string; + [verifiedSymbol]?: boolean; +} +``` + +### NIP-10 +- [NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md) + +```ts +/** On the Tags string[][] of the note, you can check if the note are: + * repost, quote, reply root, reply comment, or a simple note. + * + * filter for tags : + * ["e", "note_pubkey", "relay_url", "marker as reply | root | mention", "pubkey_author"] + * ["p", "id_note","p1_note_reply"] + * + */ +``` + diff --git a/JoyboyCommunity/src/context/NostrContext.tsx b/JoyboyCommunity/src/context/NostrContext.tsx index 4a1cb6ca..1e52274b 100644 --- a/JoyboyCommunity/src/context/NostrContext.tsx +++ b/JoyboyCommunity/src/context/NostrContext.tsx @@ -1,12 +1,15 @@ import NDK, {NDKNip07Signer} from '@nostr-dev-kit/ndk'; -import {SimplePool} from 'nostr-tools'; +import {Relay, SimplePool} from 'nostr-tools'; import {createContext, useContext, useMemo} from 'react'; -import {RELAYS_PROD} from '../utils/relay'; +import {JOYBOY_RELAYS, RELAYS_PROD} from '../utils/relay'; export type NostrContextType = { pool: SimplePool; relays: string[]; + relayJoyboy?: Relay; + othersRelays?: string[]; + connectRelayJoyboy: () => Promise; ndk: NDK; }; @@ -14,14 +17,24 @@ export const NostrContext = createContext(null); export const NostrProvider: React.FC = ({children}) => { const pool = useMemo(() => new SimplePool(), []); - const relays = RELAYS_PROD; - + const relays = JOYBOY_RELAYS; + const othersRelays = RELAYS_PROD; + + /** This Websocket connection to the Relay need to be closed after each utilization*/ + const connectRelayJoyboy = async () => { + const relayJoyboy = await Relay.connect(JOYBOY_RELAYS[0]); + return relayJoyboy; + }; const ndk = useMemo(() => { const nip07signer = new NDKNip07Signer(); return new NDK({signer: nip07signer}); }, []); - return {children}; + return ( + + {children} + + ); }; export const useNostrContext = () => { diff --git a/JoyboyCommunity/src/hooks/useNostr.tsx b/JoyboyCommunity/src/hooks/useNostr.tsx index 86aedc16..264b2ab9 100644 --- a/JoyboyCommunity/src/hooks/useNostr.tsx +++ b/JoyboyCommunity/src/hooks/useNostr.tsx @@ -1,9 +1,10 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; -import {Event as EventNostr} from 'nostr-tools'; +import {Event as EventNostr, SimplePool} from 'nostr-tools'; import {finalizeEvent, NostrEvent, parseReferences, VerifiedEvent, verifyEvent} from 'nostr-tools'; import {useNostrContext} from '../context/NostrContext'; import {IPoolEventsByQuery, IPoolEventsFromPubkey, ISendNotePayload, IUserQuery} from '../types'; +import {JOYBOY_RELAYS} from '../utils/relay'; export const useGetPoolEventById = (id: string) => { const {pool, relays} = useNostrContext(); @@ -73,16 +74,41 @@ export const useGetPoolUserQuery = ({id = '0', ...query}: IUserQuery) => { }); }; -export const useGetPoolEventsByQuery = ({ids = ['1', '3'], ...query}: IPoolEventsByQuery) => { - const {pool, relays} = useNostrContext(); +export const useGetPoolEventsByQuery = ({ + ids = ['1', '3'], + kinds = [1], + filter, + pool = new SimplePool(), + relaysToUsed, +}: IPoolEventsByQuery) => { + return useQuery({ + queryFn: () => + pool?.querySync(relaysToUsed, { + ids, + kinds, + ...filter, + }), + queryKey: ['getPoolEventsByQuery', ids, kinds, filter, relaysToUsed, pool], + placeholderData: [], + }); +}; +export const useGetPoolEventsTagsByQuery = ({ + ids, + kinds = [1], + filter, + pool = new SimplePool(), + relaysToUsed, +}: IPoolEventsByQuery) => { return useQuery({ queryFn: () => - pool.querySync(query.relaysProps ?? relays, { + pool?.querySync(relaysToUsed, { ids, - ...query.filter, + kinds, + ...filter, }), - queryKey: ['getPoolEventsByQuery', query.relaysProps, query.filter, ids], + enabled: !!filter?.search, + queryKey: ['getPoolEventsByQuery', ids, kinds, filter, pool, relaysToUsed], placeholderData: [], }); }; @@ -110,22 +136,28 @@ export const useGetUser = (pubkey: string) => { }); }; +/** Check if a note is valid and publish not on relayer if event valid */ export const useSendNote = () => { const queryClient = useQueryClient(); + const {pool, relays, connectRelayJoyboy} = useNostrContext(); return useMutation({ - mutationFn: sendNote, - onSuccess(data) { + mutationFn: isValidSendNote, + async onSuccess(data) { queryClient.invalidateQueries({ queryKey: [''], }); + const event = data?.event; + /** @TODO add option to send note on different relays and add list of relays */ + await pool.publish(JOYBOY_RELAYS, event); + return data; }, }); }; // FUNCTIONS -export const sendNote = async ({ +export const isValidSendNote = async ({ content, sk, tags, @@ -147,17 +179,17 @@ export const sendNote = async ({ const isGood = verifyEvent(event); - if (isGood) { - return { - event, - isValid: true, - }; - } else { + if (!isGood) { return { event, isValid: false, }; } + + return { + event, + isValid: true, + }; } catch (e) { console.log('issue sendNote', e); return { diff --git a/JoyboyCommunity/src/shared/components/Comments.tsx b/JoyboyCommunity/src/shared/components/Comments.tsx index 2e4fdda1..0163d0da 100644 --- a/JoyboyCommunity/src/shared/components/Comments.tsx +++ b/JoyboyCommunity/src/shared/components/Comments.tsx @@ -1,91 +1,126 @@ -import React from 'react'; -import {FlatList, View} from 'react-native'; +import {MaterialCommunityIcons} from '@expo/vector-icons'; +import {Event as EventNostr} from 'nostr-tools'; +import React, {useCallback, useEffect, useState} from 'react'; +import {ActivityIndicator, FlatList, View} from 'react-native'; +import {Input} from '../../components'; +import {useNostrContext} from '../../context/NostrContext'; +import {useGetPoolEventsTagsByQuery, useSendNote} from '../../hooks/useNostr'; +import {useAuth} from '../../store/auth'; import PostComment from './PostComment'; +import {SendComment, ViewSendComment} from './styled'; -const mockEvents = [ - { - post: { - content: 'mock content', - author: 'Roronoa zoro', +interface IComments { + event?: EventNostr; +} +function Comments({event}: IComments) { + const [text, setText] = useState(); + const {privateKey, publicKey} = useAuth(); + const {pool, relays} = useNostrContext(); + const [tags, setTags] = useState([['e', event?.id]]); - id: '1', - created_at: '2021-09-01T00:00:00Z', - pubkey: '1234567890', - }, - event: { - content: 'Lets say that One Piece is awesome', - created_at: 1680638952, - id: '12e701f23d365c558e7aa1a6f75f0477fb8593d4ad8421c4b326f8193ed42fc7', - kind: 1, - pubkey: '49e2566f8b1ef0da8aeb89865a862ead30dd5e1fc1e540edebf980a04fe9e853', - sig: 'c931395c5db7538d159d6012115b0db9b1d9a5ada52132437cddc1790982727555d0d618f109fce372f0ca0ae33d29a8d3ffae1af879e278fc2ce3c6d123c658', - tags: [ - ['e', 'a8de2eb8a069fecd33246cd921124541d6242ea76ee85c38bf45ee9b5fb3feb5'], - ['p', '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b'], - ], - }, - sourceUser: null, - }, - { - post: { - content: 'mock content', - author: 'Roronoa zoro', + const [comments, setComments] = useState([]); + const sendNote = useSendNote(); - id: '1', - created_at: '2021-09-01T00:00:00Z', - pubkey: '1234567890', + const { + data: poolEventNotesData, + isLoading: poolEventNotesDataLoading, + refetch, + } = useGetPoolEventsTagsByQuery({ + // ids: ['1'], + kinds: [1], + filter: { + '#e': tags[0], }, - event: { - content: 'What about Naruto?', - created_at: 1680638952, - id: '12e701f23d365c558e7aa1a6f75f0477fb8593d4ad8421c4b326f8193ed42fc7', - kind: 1, - pubkey: '49e2566f8b1ef0da8aeb89865a862ead30dd5e1fc1e540edebf980a04fe9e853', - sig: 'c931395c5db7538d159d6012115b0db9b1d9a5ada52132437cddc1790982727555d0d618f109fce372f0ca0ae33d29a8d3ffae1af879e278fc2ce3c6d123c658', - tags: [ - ['e', 'a8de2eb8a069fecd33246cd921124541d6242ea76ee85c38bf45ee9b5fb3feb5'], - ['p', '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b'], - ], - }, - sourceUser: null, - }, - { - post: { - content: 'mock content', - author: 'Roronoa zoro', + pool, + relaysToUsed: relays, + }); - id: '1', - created_at: '2021-09-01T00:00:00Z', - pubkey: '1234567890', - }, - event: { - content: 'Yeaaaah Naruto is sick', - created_at: 1680638952, - id: '12e701f23d365c558e7aa1a6f75f0477fb8593d4ad8421c4b326f8193ed42fc7', - kind: 1, - pubkey: '49e2566f8b1ef0da8aeb89865a862ead30dd5e1fc1e540edebf980a04fe9e853', - sig: 'c931395c5db7538d159d6012115b0db9b1d9a5ada52132437cddc1790982727555d0d618f109fce372f0ca0ae33d29a8d3ffae1af879e278fc2ce3c6d123c658', - tags: [ - ['e', 'a8de2eb8a069fecd33246cd921124541d6242ea76ee85c38bf45ee9b5fb3feb5'], - ['p', '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b'], - ], - }, - sourceUser: null, - }, -]; -function Comments() { + useEffect(() => { + const fetchReplies = async () => { + if (event?.id) { + setTags([['e', event?.id]]); + setComments(poolEventNotesData); + } + }; + fetchReplies(); + }, [event, poolEventNotesData]); + /** @TODO handle send comment */ + + const handleSendComment = useCallback(async () => { + try { + if (!privateKey) { + alert('Please login before send a note'); + return; + } + + if (!text || text?.length == 0) { + alert('Write your note'); + return; + } + + /** @TODO handle tags NIP-10 */ + /** tags */ + // let tags = [['e', event?.id, JOYBOY_RELAYS[0], 'root', publicKey]]; + const tags = [['e', event?.id, '', 'root', publicKey]]; + alert('Note sending, please wait.'); + + sendNote.mutate( + {sk: privateKey, content: text, tags}, + { + async onSuccess(data) { + if (data.isValid) { + alert('Note sent'); + } + /** Refetch comment */ + await refetch(); + }, + onError(error) { + console.log('Error send note', error); + }, + }, + ); + } catch (e) { + console.log('Error send note', e); + } + }, [text, privateKey]); + const isCreateDisabled = text && text?.length > 0 ? false : true; return ( - } - renderItem={({item}) => { - return ; - }} - /> + + + + + + + + + {poolEventNotesDataLoading && } + + } + renderItem={({item}) => { + return ; + // return ; + }} + /> + ); } diff --git a/JoyboyCommunity/src/shared/components/PostDetails.tsx b/JoyboyCommunity/src/shared/components/PostDetails.tsx index e6debff2..a00f05ba 100644 --- a/JoyboyCommunity/src/shared/components/PostDetails.tsx +++ b/JoyboyCommunity/src/shared/components/PostDetails.tsx @@ -128,7 +128,7 @@ function PostDetails({route}) { - + ); diff --git a/JoyboyCommunity/src/shared/components/styled.ts b/JoyboyCommunity/src/shared/components/styled.ts new file mode 100644 index 00000000..279cc922 --- /dev/null +++ b/JoyboyCommunity/src/shared/components/styled.ts @@ -0,0 +1,18 @@ +import {Pressable, View} from 'react-native'; +import styled from 'styled-components/native'; + +export const ViewSendComment = styled(View)` + flex-direction: row; + align-items: baseline; + border-radius: 8px; + padding: 8px 24px; + border-color: black; + color: white; +`; + +export const SendComment = styled(Pressable)` + border-radius: 8px; + padding: 8px 24px; + border-color: black; + color: white; +`; diff --git a/JoyboyCommunity/src/types/index.ts b/JoyboyCommunity/src/types/index.ts index 294ef111..4533f385 100644 --- a/JoyboyCommunity/src/types/index.ts +++ b/JoyboyCommunity/src/types/index.ts @@ -1,4 +1,4 @@ -import {Event as EventNostr, Filter} from 'nostr-tools'; +import {Event as EventNostr, Filter, SimplePool} from 'nostr-tools'; export * from './post'; export * from './routes'; @@ -54,9 +54,11 @@ export interface IPoolEventsFromPubkey { } export interface IPoolEventsByQuery { - ids: string[]; + ids?: string[]; + kinds?: number[]; filter?: Filter; - relaysProps?: string[]; + relaysToUsed?: string[]; + pool?: SimplePool; } export interface IUserQuery { diff --git a/JoyboyCommunity/src/utils/relay.ts b/JoyboyCommunity/src/utils/relay.ts index 7f01ad45..9ac9a04e 100644 --- a/JoyboyCommunity/src/utils/relay.ts +++ b/JoyboyCommunity/src/utils/relay.ts @@ -1 +1,6 @@ export const RELAYS_PROD = ['wss://relay.n057r.club', 'wss://relay.nostr.net']; + +export const JOYBOY_RELAYS = [ + 'wss://nostr.joyboy.community', + 'ws://localhost:3000', // comment if you don't run a relayer in localhost +]; diff --git a/JoyboyCommunity/tsconfig.json b/JoyboyCommunity/tsconfig.json index 73507d54..12f15398 100644 --- a/JoyboyCommunity/tsconfig.json +++ b/JoyboyCommunity/tsconfig.json @@ -1,5 +1,5 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": {}, - "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] -} +{ + "extends": "expo/tsconfig.base", + "compilerOptions": {}, + "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +} diff --git a/README.md b/README.md index 0dca754f..b0ea2feb 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,6 @@ Here are some work already merge and available for test: 1. Home page: -[Home page onboard](../resources/screens/onboard.png) onboard 2. Create Nostr account