From bfc00114d641bd63466f5fb9dea644e0c273a8ac Mon Sep 17 00:00:00 2001 From: Yury Date: Mon, 20 May 2024 16:21:43 +0300 Subject: [PATCH 01/48] feat(Relays): added logic to manage relays --- src/components/AppBar/AppBar.tsx | 16 +- src/controllers/NostrController.ts | 117 ++++++++++++++- src/hooks/index.ts | 1 + src/hooks/store.ts | 6 + src/main.tsx | 3 +- src/pages/relays/index.tsx | 227 +++++++++++++++++++++++++++++ src/pages/relays/style.module.scss | 47 ++++++ src/routes/index.tsx | 8 +- src/store/actionTypes.ts | 2 + src/store/actions.ts | 1 + src/store/relays/action.ts | 8 + src/store/relays/reducer.ts | 22 +++ src/store/relays/types.ts | 12 ++ src/store/rootReducer.ts | 6 +- src/store/store.ts | 5 +- src/types/nostr.ts | 7 + src/utils/index.ts | 1 + src/utils/utils.ts | 13 ++ 18 files changed, 494 insertions(+), 8 deletions(-) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/store.ts create mode 100644 src/pages/relays/index.tsx create mode 100644 src/pages/relays/style.module.scss create mode 100644 src/store/relays/action.ts create mode 100644 src/store/relays/reducer.ts create mode 100644 src/store/relays/types.ts create mode 100644 src/utils/utils.ts diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 40ca9de..207dd01 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -18,7 +18,11 @@ import Username from '../username' import { Link, useNavigate } from 'react-router-dom' import nostrichAvatar from '../../assets/images/avatar.png' import { NostrController } from '../../controllers' -import { appPublicRoutes, getProfileRoute } from '../../routes' +import { + appPrivateRoutes, + appPublicRoutes, + getProfileRoute +} from '../../routes' import { clearAuthToken, saveNsecBunkerDelegatedKey, @@ -143,6 +147,16 @@ export const AppBar = () => { > Profile + { + navigate(appPrivateRoutes.relays) + }} + sx={{ + justifyContent: 'center' + }} + > + Relays + { if (res.status === 'rejected') { failedPublishes.push({ relay: relays[index], - error: res.reason.message + error: res.reason + ? res.reason.message || fallbackRejectionReason + : fallbackRejectionReason }) } }) @@ -364,6 +371,110 @@ export class NostrController extends EventEmitter { return Promise.resolve(pubKey) } + /** + * Provides relay map. + * @param npub - user's npub + * @returns - promise that resolves into relay map and a timestamp when it has been updated. + */ + getRelayMap = async ( + npub: string + ): Promise<{ map: RelayMap; mapUpdated: number }> => { + const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS + const hardcodedPopularRelays = (mostPopularRelays || '').split(' ') + const popularRelayURIs = [ + this.specialMetadataRelay, + ...hardcodedPopularRelays + ] + + const pool = new SimplePool() + + // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md + const eventFilter: Filter = { + kinds: [kinds.RelayList], + authors: [npub] + } + + const event = await pool.get(popularRelayURIs, eventFilter).catch((err) => { + return Promise.reject(err) + }) + + if (event) { + // Handle founded 10002 event + const relaysMap: RelayMap = {} + + // 'r' stands for 'relay' + const relayTags = event.tags.filter((tag) => tag[0] === 'r') + + relayTags.forEach((tag) => { + const uri = tag[1] + const relayType = tag[2] + + // if 3rd element of relay tag is undefined, relay is WRITE and READ + relaysMap[uri] = { + write: relayType ? relayType === 'write' : true, + read: relayType ? relayType === 'read' : true + } + }) + + return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at }) + } else { + return Promise.reject('User relays were not found.') + } + } + + /** + * Publishes relay map. + * @param relayMap - relay map. + * @param npub - user's npub. + * @returns - promise that resolves into a string representing publishing result. + */ + publishRelayMap = async ( + relayMap: RelayMap, + npub: string + ): Promise => { + const timestamp = Math.floor(Date.now() / 1000) + const relayURIs = Object.keys(relayMap) + + // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md + const tags: string[][] = relayURIs.map((relayURI) => + [ + 'r', + relayURI, + relayMap[relayURI].read && relayMap[relayURI].write + ? '' + : relayMap[relayURI].write + ? 'write' + : 'read' + ].filter((value) => value !== '') + ) + + const newRelayMapEvent: UnsignedEvent = { + kind: kinds.RelayList, + tags, + content: '', + pubkey: npub, + created_at: timestamp + } + + const signedEvent = await this.signEvent(newRelayMapEvent) + + let relaysToPublish = relayURIs + + // If relay map is empty, use most popular relay URIs + if (!relaysToPublish.length) { + const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS + const hardcodedPopularRelays = (mostPopularRelays || '').split(' ') + + relaysToPublish = [this.specialMetadataRelay, ...hardcodedPopularRelays] + } + + await this.publishEvent(signedEvent, relaysToPublish) + + return Promise.resolve( + `Relay Map published on: ${relaysToPublish.join('\n')}` + ) + } + /** * Generates NDK Private Signer * @returns nSecBunker delegated key diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..16c8633 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './store' diff --git a/src/hooks/store.ts b/src/hooks/store.ts new file mode 100644 index 0000000..f3e9b21 --- /dev/null +++ b/src/hooks/store.ts @@ -0,0 +1,6 @@ +import { useDispatch, useSelector } from 'react-redux' +import type { Dispatch, RootState } from '../store/store' + +// Use instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() diff --git a/src/main.tsx b/src/main.tsx index 0b362fc..4da2ffd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,7 +14,8 @@ store.subscribe( _.throttle(() => { saveState({ auth: store.getState().auth, - metadata: store.getState().metadata + metadata: store.getState().metadata, + relays: store.getState().relays }) }, 1000) ) diff --git a/src/pages/relays/index.tsx b/src/pages/relays/index.tsx new file mode 100644 index 0000000..3ee558c --- /dev/null +++ b/src/pages/relays/index.tsx @@ -0,0 +1,227 @@ +import { useEffect, useState } from 'react' +import { Box, List, ListItem, TextField } from '@mui/material' +import RouterIcon from '@mui/icons-material/Router' +import styles from './style.module.scss' +import Switch from '@mui/material/Switch' +import ListItemText from '@mui/material/ListItemText' +import Divider from '@mui/material/Divider' +import { NostrController } from '../../controllers' +import { RelayMap } from '../../types' +import LogoutIcon from '@mui/icons-material/Logout' +import { useAppSelector, useAppDispatch } from '../../hooks' +import { compareObjects } from '../../utils' +import { setRelayMapAction } from '../../store/actions' +import { toast } from 'react-toastify' + +export const RelaysPage = () => { + const nostrController = NostrController.getInstance() + + const relaysState = useAppSelector((state) => state.relays) + const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey) + + const dispatch = useAppDispatch() + + const [newRelayURI, setNewRelayURI] = useState() + const [newRelayURIerror, setNewRelayURIerror] = useState() + const [relayMap, setRelayMap] = useState( + relaysState?.map + ) + + useEffect(() => { + let isMounted = false + + const fetchData = async () => { + if (usersPubkey) { + isMounted = true + + // call async func to fetch relay map + const newRelayMap = await nostrController.getRelayMap(usersPubkey) + + // handle fetched relay map + if (isMounted) { + if ( + !relaysState?.mapUpdated || + newRelayMap.mapUpdated > relaysState?.mapUpdated + ) + if ( + !relaysState?.map || + !compareObjects(relaysState.map, newRelayMap) + ) { + setRelayMap(newRelayMap.map) + + dispatch(setRelayMapAction(newRelayMap.map)) + } + } + } + } + + // Publishing relay map can take some time. + // This is why data fetch should happen only if relay map was received more than 5 minutes ago. + if ( + usersPubkey && + (!relaysState?.mapUpdated || + Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes + ) { + fetchData() + } + + // cleanup func + return () => { + isMounted = false + } + }, [dispatch, usersPubkey, relaysState, nostrController]) + + const relayRequirementWarning = () => + toast.warning('At least one write relay is needed for SIGit to work.') + + const handleLeaveRelay = async (relay: string) => { + if (relayMap) { + if (Object.keys(relayMap).length === 1) { + relayRequirementWarning() + } else { + const relayMapCopy = JSON.parse(JSON.stringify(relayMap)) + // Remove relay from relay map + delete relayMapCopy[relay] + + setRelayMap(relayMapCopy) + + dispatch(setRelayMapAction(relayMapCopy)) + + if (usersPubkey) { + // Publish updated relay map. + const relayMapPublishingRes = await nostrController + .publishRelayMap(relayMapCopy, usersPubkey) + .catch((err) => handlePublishRelayMapError(err)) + + if (relayMapPublishingRes) toast.success(relayMapPublishingRes) + } + } + } + } + + const handlePublishRelayMapError = (err: any) => { + const errorPrefix = 'Error while publishing Relay Map' + + if (Array.isArray(err)) { + err.forEach((errorObj: { relay: string; error: string }) => { + toast.error( + `${errorPrefix} to ${errorObj.relay}. Error: ${errorObj.error || 'Unknown'}` + ) + }) + } else { + toast.error(`${errorPrefix}. Error: ${err.message || 'Unknown'}`) + } + } + + const handleRelayWriteChange = async ( + relay: string, + event: React.ChangeEvent + ) => { + if (relayMap && relayMap[relay]) { + if ( + !event.target.checked && + Object.keys(relayMap).filter((relay) => relayMap[relay].write) + .length === 1 + ) { + relayRequirementWarning() + } else { + const relayMapCopy = JSON.parse(JSON.stringify(relayMap)) + relayMapCopy[relay].write = event.target.checked + + setRelayMap(relayMapCopy) + + dispatch(setRelayMapAction(relayMapCopy)) + + if (usersPubkey) { + // Publish updated relay map + const relayMapPublishingRes = await nostrController + .publishRelayMap(relayMapCopy, usersPubkey) + .catch((err) => handlePublishRelayMapError(err)) + + if (relayMapPublishingRes) toast.success(relayMapPublishingRes) + } + } + } + } + + const handleTextFieldChange = async () => { + // Check if new relay URI is a valid string + if ( + newRelayURI && + !/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test( + newRelayURI + ) + ) { + setNewRelayURIerror( + 'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io' + ) + } else if (newRelayURI) { + const relayMapCopy = JSON.parse(JSON.stringify(relayMap)) + + relayMapCopy[newRelayURI.trim()] = { write: true, read: true } + + setRelayMap(relayMapCopy) + setNewRelayURI('') + + dispatch(setRelayMapAction(relayMapCopy)) + + if (usersPubkey) { + // Publish updated relay map + const relayMapPublishingRes = await nostrController + .publishRelayMap(relayMapCopy, usersPubkey) + .catch((err) => handlePublishRelayMapError(err)) + + if (relayMapPublishingRes) toast.success(relayMapPublishingRes) + } + } + } + + return ( + + + handleTextFieldChange()} + onChange={(e) => setNewRelayURI(e.target.value)} + helperText={newRelayURIerror} + error={!!newRelayURIerror} + placeholder="wss://" + className={styles.relayURItextfield} + /> + + + + YOUR RELAYS + + {relayMap && ( + + {Object.keys(relayMap).map((relay, i) => ( + + + + + handleLeaveRelay(relay)} + > + + Leave + + + + + + handleRelayWriteChange(relay, event)} + /> + + + + ))} + + )} + + ) +} diff --git a/src/pages/relays/style.module.scss b/src/pages/relays/style.module.scss new file mode 100644 index 0000000..dd9d706 --- /dev/null +++ b/src/pages/relays/style.module.scss @@ -0,0 +1,47 @@ +@import '../../colors.scss'; + +.container { + margin-top: 25px; + + .relayURItextfield { + width: 100%; + } + + .sectionIcon { + font-size: 30px; + } + + .sectionTitle { + margin-top: 35px; + margin-bottom: 10px; + display: flex; + flex-direction: row; + gap: 5px; + font-size: 1.5rem; + line-height: 2rem; + font-weight: 600; + } + + .relaysContainer { + display: flex; + flex-direction: column; + gap: 15px; + } + + .relay { + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 4px; + + .relayDivider { + margin-left: 10px; + margin-right: 10px; + } + + .leaveRelayContainer { + display: flex; + flex-direction: row; + gap: 10px; + cursor: pointer; + } + } +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index dbbf728..c764f98 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -5,11 +5,13 @@ import { Login } from '../pages/login' import { ProfilePage } from '../pages/profile' import { hexToNpub } from '../utils' import { VerifyPage } from '../pages/verify' +import { RelaysPage } from '../pages/relays' export const appPrivateRoutes = { homePage: '/', create: '/create', - verify: '/verify' + verify: '/verify', + relays: '/relays' } export const appPublicRoutes = { @@ -51,5 +53,9 @@ export const privateRoutes = [ { path: appPrivateRoutes.verify, element: + }, + { + path: appPrivateRoutes.relays, + element: } ] diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index d3047a6..524e81f 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -7,3 +7,5 @@ export const UPDATE_NSECBUNKER_PUBKEY = 'UPDATE_NSECBUNKER_PUBKEY' export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS' export const SET_METADATA_EVENT = 'SET_METADATA_EVENT' + +export const SET_RELAY_MAP = 'SET_RELAY_MAP' diff --git a/src/store/actions.ts b/src/store/actions.ts index 3bf716b..7512f80 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -3,6 +3,7 @@ import { State } from './rootReducer' export * from './auth/action' export * from './metadata/action' +export * from './relays/action' export const restoreState = (payload: State) => { return { diff --git a/src/store/relays/action.ts b/src/store/relays/action.ts new file mode 100644 index 0000000..ff18724 --- /dev/null +++ b/src/store/relays/action.ts @@ -0,0 +1,8 @@ +import * as ActionTypes from '../actionTypes' +import { SetRelayMapAction } from './types' +import { RelayMap } from '../../types' + +export const setRelayMapAction = (payload: RelayMap): SetRelayMapAction => ({ + type: ActionTypes.SET_RELAY_MAP, + payload +}) diff --git a/src/store/relays/reducer.ts b/src/store/relays/reducer.ts new file mode 100644 index 0000000..5febd1b --- /dev/null +++ b/src/store/relays/reducer.ts @@ -0,0 +1,22 @@ +import * as ActionTypes from '../actionTypes' +import { RelaysDispatchTypes, RelaysState } from './types' + +const initialState: RelaysState | null = null + +const reducer = ( + state = initialState, + action: RelaysDispatchTypes +): RelaysState | null => { + switch (action.type) { + case ActionTypes.SET_RELAY_MAP: + return { map: action.payload, mapUpdated: Date.now() } + + case ActionTypes.RESTORE_STATE: + return action.payload.relays + + default: + return state + } +} + +export default reducer diff --git a/src/store/relays/types.ts b/src/store/relays/types.ts new file mode 100644 index 0000000..3a86aac --- /dev/null +++ b/src/store/relays/types.ts @@ -0,0 +1,12 @@ +import * as ActionTypes from '../actionTypes' +import { RestoreState } from '../actions' +import { RelayMap } from '../../types' + +export type RelaysState = { map: RelayMap; mapUpdated: number } + +export interface SetRelayMapAction { + type: typeof ActionTypes.SET_RELAY_MAP + payload: RelayMap +} + +export type RelaysDispatchTypes = SetRelayMapAction | RestoreState diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index f56217f..7857e4a 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -3,13 +3,17 @@ import { combineReducers } from 'redux' import authReducer from './auth/reducer' import { AuthState } from './auth/types' import metadataReducer from './metadata/reducer' +import { RelaysState } from './relays/types' +import relaysReducer from './relays/reducer' export interface State { auth: AuthState + relays: RelaysState metadata?: Event } export default combineReducers({ auth: authReducer, - metadata: metadataReducer + metadata: metadataReducer, + relays: relaysReducer }) diff --git a/src/store/store.ts b/src/store/store.ts index 20d9b66..ab20121 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,8 +1,11 @@ import { configureStore } from '@reduxjs/toolkit' import rootReducer from './rootReducer' -const store = configureStore({ reducer: rootReducer }) +const store = configureStore({ + reducer: rootReducer +}) export default store export type Dispatch = typeof store.dispatch +export type RootState = ReturnType diff --git a/src/types/nostr.ts b/src/types/nostr.ts index 67572d8..d3a0cef 100644 --- a/src/types/nostr.ts +++ b/src/types/nostr.ts @@ -17,3 +17,10 @@ export interface NostrJoiningBlock { block: number encodedEventPointer: string } + +export type RelayMap = { + [key: string]: { + read: boolean + write: boolean + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 156c643..d2f5ec1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './misc' export * from './nostr' export * from './string' export * from './zip' +export * from './utils' diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..a436fae --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,13 @@ +export const compareObjects = ( + obj1: object | null | undefined, + obj2: object | null | undefined +): boolean => { + if (Array.isArray(obj1) && Array.isArray(obj2)) { + const obj1Copy = [...obj1].sort() + const obj2Copy = [...obj2].sort() + + return JSON.stringify(obj1Copy) === JSON.stringify(obj2Copy) + } + + return JSON.stringify(obj1) === JSON.stringify(obj2) +} -- 2.34.1 From 7068d85821d7b2b55923bda9354d00eb788b1483 Mon Sep 17 00:00:00 2001 From: Yury Date: Mon, 20 May 2024 17:17:29 +0300 Subject: [PATCH 02/48] feat(Relays): added fetching relays on authenticate --- src/controllers/AuthController.ts | 32 ++++++++++++++++++++++++++++--- src/pages/login/index.tsx | 8 ++++---- src/pages/relays/index.tsx | 7 +++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 8e488c5..1e853d1 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,13 +1,18 @@ import { EventTemplate } from 'nostr-tools' import { MetadataController, NostrController } from '.' -import { setAuthState, setMetadataEvent } from '../store/actions' +import { + setAuthState, + setMetadataEvent, + setRelayMapAction +} from '../store/actions' import store from '../store/store' import { base64DecodeAuthToken, base64EncodeSignedEvent, getAuthToken, getVisitedLink, - saveAuthToken + saveAuthToken, + compareObjects } from '../utils' import { appPrivateRoutes } from '../routes' import { SignedEvent } from '../types' @@ -30,7 +35,7 @@ export class AuthController { * @returns url to redirect if authentication successfull * or error if otherwise */ - async authenticateAndFindMetadata(pubkey: string) { + async authAndGetMetadataAndRelaysMap(pubkey: string) { this.metadataController .findMetadata(pubkey) .then((event) => { @@ -61,6 +66,27 @@ export class AuthController { }) ) + const relaysState = store.getState().relays + + if (relaysState) { + // Relays state is defined and there is no need to await for the latest relay map + this.nostrController.getRelayMap(pubkey).then((relayMap) => { + if (!compareObjects(relaysState?.map, relayMap)) { + store.dispatch(setRelayMapAction(relayMap.map)) + } + }) + } else { + // Relays state is not defined, await for the latest relay map + const relayMap = await this.nostrController.getRelayMap(pubkey) + + if (Object.keys(relayMap).length < 1) { + // Navigate user to relays page + return Promise.resolve(appPrivateRoutes.relays) + } + + store.dispatch(setRelayMapAction(relayMap.map)) + } + const visitedLink = getVisitedLink() if (visitedLink) { diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 7d26348..bc1366a 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -54,7 +54,7 @@ export const Login = () => { setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = - await authController.authenticateAndFindMetadata(pubkey) + await authController.authAndGetMetadataAndRelaysMap(pubkey) navigate(redirectPath) }) @@ -93,7 +93,7 @@ export const Login = () => { setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController - .authenticateAndFindMetadata(publickey) + .authAndGetMetadataAndRelaysMap(publickey) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null @@ -188,7 +188,7 @@ export const Login = () => { setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController - .authenticateAndFindMetadata(pubkey!) + .authAndGetMetadataAndRelaysMap(pubkey!) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null @@ -248,7 +248,7 @@ export const Login = () => { setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController - .authenticateAndFindMetadata(pubkey!) + .authAndGetMetadataAndRelaysMap(pubkey!) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null diff --git a/src/pages/relays/index.tsx b/src/pages/relays/index.tsx index 3ee558c..ad1626a 100644 --- a/src/pages/relays/index.tsx +++ b/src/pages/relays/index.tsx @@ -176,6 +176,13 @@ export const RelaysPage = () => { } } + useEffect(() => { + // Display notification if an empty relay map has been received + if (relayMap && Object.keys(relayMap).length === 0) { + relayRequirementWarning() + } + }, [relayMap]) + return ( -- 2.34.1 From be2204c65d646b3df2bb8beb85ed9abcb595be7c Mon Sep 17 00:00:00 2001 From: Davinci Date: Wed, 15 May 2024 13:14:21 +0200 Subject: [PATCH 03/48] fix: placeholder avatar is incosistent across components --- src/pages/create/index.tsx | 2 +- src/pages/profile/index.tsx | 2 +- src/pages/verify/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index b7e568d..7d072ab 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -21,7 +21,7 @@ import { import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' -import placeholderAvatar from '../../assets/images/nostr-logo.jpg' +import placeholderAvatar from '../../assets/images/avatar.png' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { appPrivateRoutes, getProfileRoute } from '../../routes' diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 5a38b2f..6a2ade5 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -15,7 +15,7 @@ import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' -import placeholderAvatar from '../../assets/images/nostr-logo.jpg' +import placeholderAvatar from '../../assets/images/avatar.png' import { MetadataController, NostrController } from '../../controllers' import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6f165b9..7b2a2bd 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,7 +23,7 @@ import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Link, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' -import placeholderAvatar from '../../assets/images/nostr-logo.jpg' +import placeholderAvatar from '../../assets/images/avatar.png' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { getProfileRoute } from '../../routes' -- 2.34.1 From 8be01d96dd5ba2df01ab5ce119f89a2192818770 Mon Sep 17 00:00:00 2001 From: Davinci Date: Wed, 15 May 2024 13:57:54 +0200 Subject: [PATCH 04/48] fix: remove nostr image for placeholder avatar, use robohash instead --- src/components/AppBar/AppBar.tsx | 12 +++++++++--- src/pages/create/index.tsx | 9 +++++---- src/pages/profile/index.tsx | 6 +++--- src/pages/verify/index.tsx | 15 ++++++++------- src/utils/nostr.ts | 5 +++++ 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 207dd01..7312621 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -16,7 +16,6 @@ import { Dispatch } from '../../store/store' import Username from '../username' import { Link, useNavigate } from 'react-router-dom' -import nostrichAvatar from '../../assets/images/avatar.png' import { NostrController } from '../../controllers' import { appPrivateRoutes, @@ -25,6 +24,8 @@ import { } from '../../routes' import { clearAuthToken, + getRoboHashPicture, + hexToNpub, saveNsecBunkerDelegatedKey, shorten } from '../../utils' @@ -36,7 +37,7 @@ export const AppBar = () => { const dispatch: Dispatch = useDispatch() const [username, setUsername] = useState('') - const [userAvatar, setUserAvatar] = useState(nostrichAvatar) + const [userAvatar, setUserAvatar] = useState('') const [anchorElUser, setAnchorElUser] = useState(null) const authState = useSelector((state: State) => state.auth) @@ -45,8 +46,13 @@ export const AppBar = () => { useEffect(() => { if (metadataState && metadataState.content) { const { picture, display_name, name } = JSON.parse(metadataState.content) + const npub = authState?.usersPubkey ? hexToNpub(authState.usersPubkey) : '' - if (picture) setUserAvatar(picture) + if (picture) { + setUserAvatar(picture) + } else { + setUserAvatar(getRoboHashPicture(npub)) + } setUsername(shorten(display_name || name || '', 7)) } diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 7d072ab..c154385 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -21,7 +21,6 @@ import { import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' -import placeholderAvatar from '../../assets/images/avatar.png' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { appPrivateRoutes, getProfileRoute } from '../../routes' @@ -30,6 +29,7 @@ import { encryptArrayBuffer, generateEncryptionKey, getHash, + getRoboHashPicture, hexToNpub, pubToHex, queryNip05, @@ -489,8 +489,9 @@ const DisplayUser = ({ }) }, [users]) - const imageLoadError = (event: any) => { - event.target.src = placeholderAvatar + const imageLoadError = (event: any, pubkey: string) => { + const npub = hexToNpub(pubkey) + event.target.src = getRoboHashPicture(npub) } return ( @@ -513,7 +514,7 @@ const DisplayUser = ({ {imageLoadError(event, user.pubkey)}} src={userMeta?.picture || roboUrl} alt="Profile Image" className="profile-image" diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 6a2ade5..e57e9f7 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -15,7 +15,6 @@ import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' -import placeholderAvatar from '../../assets/images/avatar.png' import { MetadataController, NostrController } from '../../controllers' import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' @@ -27,6 +26,7 @@ import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' import { SmartToy } from '@mui/icons-material' +import { getRoboHashPicture } from '../../utils' export const ProfilePage = () => { const theme = useTheme() @@ -282,13 +282,13 @@ export const ProfilePage = () => { > { - event.target.src = placeholderAvatar + event.target.src = npub ? getRoboHashPicture(npub) : '' }} onLoad={() => { setAvatarLoading(false) }} className={styles.img} - src={profileMetadata.picture || placeholderAvatar} + src={profileMetadata.picture || npub ? getRoboHashPicture(npub!) : ''} alt="Profile Image" /> diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 7b2a2bd..af7da88 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -23,7 +23,6 @@ import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Link, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' -import placeholderAvatar from '../../assets/images/avatar.png' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { getProfileRoute } from '../../routes' @@ -34,6 +33,7 @@ import { encryptArrayBuffer, generateEncryptionKey, getHash, + getRoboHashPicture, hexToNpub, parseJson, readContentOfZipEntry, @@ -626,13 +626,14 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { }) }, [users, meta.submittedBy]) - const imageLoadError = (event: any) => { - event.target.src = placeholderAvatar + const imageLoadError = (event: any, pubkey: string) => { + const npub = hexToNpub(pubkey) + event.target.src = npub } const getRoboImageUrl = (pubkey: string) => { const npub = hexToNpub(pubkey) - return `https://robohash.org/${npub}.png?set=set3` + return getRoboHashPicture(npub) } return ( @@ -666,7 +667,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { {imageLoadError(event, meta.submittedBy)}} src={ metadata[meta.submittedBy]?.picture || getRoboImageUrl(meta.submittedBy) @@ -719,7 +720,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { {users.map((user, index) => { const userMeta = metadata[user.pubkey] const npub = hexToNpub(user.pubkey) - const roboUrl = `https://robohash.org/${npub}.png?set=set3` + const roboUrl = getRoboHashPicture(npub) let signedStatus = '-' @@ -739,7 +740,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { {imageLoadError(event, meta.submittedBy)}} src={userMeta?.picture || roboUrl} alt="Profile Image" className="profile-image" diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 8f074c0..d3eba8b 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -58,6 +58,7 @@ export const nsecToHex = (nsec: string): string | null => { export const hexToNpub = (hexPubkey: string | undefined): string => { if (!hexPubkey) return 'n/a' + if (hexPubkey.includes('npub')) return hexPubkey return nip19.npubEncode(hexPubkey) } @@ -138,3 +139,7 @@ export const base64DecodeAuthToken = (authToken: string): SignedEvent => { throw new Error('An error occurred in JSON.parse of the auth token') } } + +export const getRoboHashPicture = (npub: string): string => { + return `https://robohash.org/${npub}.png?set=set3` +} -- 2.34.1 From b2d746112858e89bb8ef4a0b6265657904e0cd1b Mon Sep 17 00:00:00 2001 From: Davinci Date: Wed, 15 May 2024 11:57:35 +0200 Subject: [PATCH 05/48] fix: entering decryption key manually does not work because of encoded URI --- src/pages/decrypt/index.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/decrypt/index.tsx b/src/pages/decrypt/index.tsx index 60edb35..be699c5 100644 --- a/src/pages/decrypt/index.tsx +++ b/src/pages/decrypt/index.tsx @@ -24,20 +24,22 @@ export const DecryptZip = () => { const fileUrl = searchParams.get('file') if (fileUrl) { + const decodedFileUrl = decodeURIComponent(fileUrl) + setIsLoading(true) setLoadingSpinnerDesc('Fetching zip file') axios - .get(fileUrl, { + .get(decodedFileUrl, { responseType: 'arraybuffer' }) .then((res) => { - const fileName = fileUrl.split('/').pop() + const fileName = decodedFileUrl.split('/').pop() const file = new File([res.data], fileName!) setSelectedFile(file) }) .catch((err) => { console.error( - `error occurred in getting zip file from ${fileUrl}`, + `error occurred in getting zip file from ${decodedFileUrl}`, err ) }) @@ -47,7 +49,10 @@ export const DecryptZip = () => { } const key = searchParams.get('key') - if (key) setEncryptionKey(key) + if (key) { + const decodedKey = decodeURIComponent(key) + setEncryptionKey(decodedKey) + } }, [searchParams]) const handleDecrypt = async () => { -- 2.34.1 From a2232cd4202fde5eed750602760b80cbb0118b8a Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 08:55:24 +0200 Subject: [PATCH 06/48] fix: getRobohash function will do the conversion of pubkey --- src/components/AppBar/AppBar.tsx | 4 ++-- src/pages/create/index.tsx | 3 +-- src/pages/verify/index.tsx | 6 ++---- src/utils/nostr.ts | 8 +++++++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 7312621..eef58b7 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -46,12 +46,12 @@ export const AppBar = () => { useEffect(() => { if (metadataState && metadataState.content) { const { picture, display_name, name } = JSON.parse(metadataState.content) - const npub = authState?.usersPubkey ? hexToNpub(authState.usersPubkey) : '' + const pubkey = authState?.usersPubkey || '' if (picture) { setUserAvatar(picture) } else { - setUserAvatar(getRoboHashPicture(npub)) + setUserAvatar(getRoboHashPicture(pubkey)) } setUsername(shorten(display_name || name || '', 7)) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index c154385..2847c76 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -490,8 +490,7 @@ const DisplayUser = ({ }, [users]) const imageLoadError = (event: any, pubkey: string) => { - const npub = hexToNpub(pubkey) - event.target.src = getRoboHashPicture(npub) + event.target.src = getRoboHashPicture(pubkey) } return ( diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index af7da88..9a884d3 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -632,8 +632,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { } const getRoboImageUrl = (pubkey: string) => { - const npub = hexToNpub(pubkey) - return getRoboHashPicture(npub) + return getRoboHashPicture(pubkey) } return ( @@ -719,8 +718,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { {users.map((user, index) => { const userMeta = metadata[user.pubkey] - const npub = hexToNpub(user.pubkey) - const roboUrl = getRoboHashPicture(npub) + const roboUrl = getRoboHashPicture(user.pubkey) let signedStatus = '-' diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index d3eba8b..a39502f 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -140,6 +140,12 @@ export const base64DecodeAuthToken = (authToken: string): SignedEvent => { } } -export const getRoboHashPicture = (npub: string): string => { +/** + * + * @param pubkey in hex or npub format + * @returns robohash.org url for the avatar + */ +export const getRoboHashPicture = (pubkey: string): string => { + const npub = hexToNpub(pubkey) return `https://robohash.org/${npub}.png?set=set3` } -- 2.34.1 From 05e8a5dc4b4478595676efc3f6f6173187d5970c Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 09:13:47 +0200 Subject: [PATCH 07/48] chore: typo --- src/pages/verify/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 9a884d3..b7900d4 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -718,7 +718,8 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { {users.map((user, index) => { const userMeta = metadata[user.pubkey] - const roboUrl = getRoboHashPicture(user.pubkey) + const npub = hexToNpub(user.pubkey) + const roboUrl = getRoboHashPicture(npub) let signedStatus = '-' -- 2.34.1 From b3954d387a23d1fbf047307beb04e08e5a5e6f61 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 09:23:08 +0200 Subject: [PATCH 08/48] fix: removal of create nostr auth token --- src/controllers/AuthController.ts | 14 -------------- src/pages/verify/index.tsx | 4 ++++ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 1e853d1..bdb4410 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -45,20 +45,6 @@ export class AuthController { console.error('Error occurred while finding metadata', err) }) - // Nostr uses unix timestamps - const timestamp = Math.floor(Date.now() / 1000) - const { hostname } = window.location - - const authEvent: EventTemplate = { - kind: 1, - tags: [], - content: `${hostname}-${timestamp}`, - created_at: timestamp - } - - const signedAuthEvent = await this.nostrController.signEvent(authEvent) - this.createAndSaveAuthToken(signedAuthEvent) - store.dispatch( setAuthState({ loggedIn: true, diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index b7900d4..09133e8 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -388,6 +388,10 @@ export const VerifyPage = () => { setIsLoading(false) } + /** + * + * @returns exported.zip including signed files and meta info (about signers and viewers) + */ const handleExport = async () => { if (!meta || !zip || !usersPubkey) return -- 2.34.1 From 150e7a447c78e59f8d2b2ee782e01a539700a7e6 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Wed, 15 May 2024 16:03:31 +0500 Subject: [PATCH 09/48] chore: remove unused code --- src/pages/decrypt/index.tsx | 146 ---------------------------- src/pages/decrypt/style.module.scss | 27 ----- 2 files changed, 173 deletions(-) delete mode 100644 src/pages/decrypt/index.tsx delete mode 100644 src/pages/decrypt/style.module.scss diff --git a/src/pages/decrypt/index.tsx b/src/pages/decrypt/index.tsx deleted file mode 100644 index be699c5..0000000 --- a/src/pages/decrypt/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Box, Button, TextField, Typography } from '@mui/material' -import saveAs from 'file-saver' -import { MuiFileInput } from 'mui-file-input' -import { useEffect, useState } from 'react' -import { LoadingSpinner } from '../../components/LoadingSpinner' -import { decryptArrayBuffer } from '../../utils' -import styles from './style.module.scss' -import { toast } from 'react-toastify' -import { useSearchParams } from 'react-router-dom' -import axios from 'axios' -import { DecryptionError } from '../../types/errors/DecryptionError' - -export const DecryptZip = () => { - const [searchParams] = useSearchParams() - - const [selectedFile, setSelectedFile] = useState(null) - const [encryptionKey, setEncryptionKey] = useState('') - - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - const [isDraggingOver, setIsDraggingOver] = useState(false) - - useEffect(() => { - const fileUrl = searchParams.get('file') - - if (fileUrl) { - const decodedFileUrl = decodeURIComponent(fileUrl) - - setIsLoading(true) - setLoadingSpinnerDesc('Fetching zip file') - axios - .get(decodedFileUrl, { - responseType: 'arraybuffer' - }) - .then((res) => { - const fileName = decodedFileUrl.split('/').pop() - const file = new File([res.data], fileName!) - setSelectedFile(file) - }) - .catch((err) => { - console.error( - `error occurred in getting zip file from ${decodedFileUrl}`, - err - ) - }) - .finally(() => { - setIsLoading(false) - }) - } - - const key = searchParams.get('key') - if (key) { - const decodedKey = decodeURIComponent(key) - setEncryptionKey(decodedKey) - } - }, [searchParams]) - - const handleDecrypt = async () => { - if (!selectedFile || !encryptionKey) return - - setIsLoading(true) - setLoadingSpinnerDesc('Decrypting zip file') - - const encryptedArrayBuffer = await selectedFile.arrayBuffer() - - const arrayBuffer = await decryptArrayBuffer( - encryptedArrayBuffer, - encryptionKey - ).catch((err: DecryptionError) => { - console.log('err in decryption:>> ', err) - - toast.error(err.message) - setIsLoading(false) - return null - }) - - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, 'decrypted.zip') - - setIsLoading(false) - } - - const handleDrop = (event: React.DragEvent) => { - event.preventDefault() - setIsDraggingOver(false) - const file = event.dataTransfer.files[0] - if (file.type === 'application/zip') setSelectedFile(file) - } - - const handleDragOver = (event: React.DragEvent) => { - event.preventDefault() - setIsDraggingOver(true) - } - - return ( - <> - {isLoading && } - - - Select encrypted zip file - - - - {isDraggingOver && ( - - Drop file here - - )} - setSelectedFile(value)} - InputProps={{ - inputProps: { - accept: '.zip' - } - }} - /> - - setEncryptionKey(e.target.value)} - /> - - - - - - - - ) -} diff --git a/src/pages/decrypt/style.module.scss b/src/pages/decrypt/style.module.scss deleted file mode 100644 index ae72e77..0000000 --- a/src/pages/decrypt/style.module.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import '../../colors.scss'; - -.container { - display: flex; - flex-direction: column; - color: $text-color; - - .inputBlock { - position: relative; - display: flex; - flex-direction: column; - gap: 25px; - } - - .fileDragOver { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(255, 255, 255, 0.8); - z-index: 1; - display: flex; - justify-content: center; - align-items: center; - } -} -- 2.34.1 From b5761ac86b35f94aa7a06b0a254564cd47f5443e Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Wed, 15 May 2024 16:11:57 +0500 Subject: [PATCH 10/48] chore: convert verify to sign --- src/pages/create/index.tsx | 15 ++++++--------- src/pages/home/index.tsx | 4 ++-- src/pages/{verify => sign}/index.tsx | 2 +- src/pages/{verify => sign}/style.module.scss | 0 src/routes/index.tsx | 13 +++++++------ src/utils/misc.ts | 2 +- 6 files changed, 17 insertions(+), 19 deletions(-) rename src/pages/{verify => sign}/index.tsx (99%) rename src/pages/{verify => sign}/style.module.scss (100%) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 2847c76..c308026 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -20,10 +20,10 @@ import { } from '@mui/material' import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' -import { appPrivateRoutes, getProfileRoute } from '../../routes' +import { getProfileRoute } from '../../routes' import { ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, @@ -45,7 +45,6 @@ import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' export const CreatePage = () => { - const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -350,11 +349,7 @@ export const CreatePage = () => { } setIsLoading(false) - navigate( - `${appPrivateRoutes.verify}?file=${encodeURIComponent( - fileUrl - )}&key=${encodeURIComponent(encryptionKey)}` - ) + // todo: navigate to verify } if (authUrl) { @@ -513,7 +508,9 @@ const DisplayUser = ({ {imageLoadError(event, user.pubkey)}} + onError={(event) => { + imageLoadError(event, user.pubkey) + }} src={userMeta?.picture || roboUrl} alt="Profile Image" className="profile-image" diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 01ee591..1b46e58 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -15,10 +15,10 @@ export const HomePage = () => { Create ) diff --git a/src/pages/verify/index.tsx b/src/pages/sign/index.tsx similarity index 99% rename from src/pages/verify/index.tsx rename to src/pages/sign/index.tsx index 09133e8..632a885 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/sign/index.tsx @@ -50,7 +50,7 @@ enum SignedStatus { User_Is_Not_Next_Signer } -export const VerifyPage = () => { +export const SignPage = () => { const [searchParams] = useSearchParams() const [displayInput, setDisplayInput] = useState(false) diff --git a/src/pages/verify/style.module.scss b/src/pages/sign/style.module.scss similarity index 100% rename from src/pages/verify/style.module.scss rename to src/pages/sign/style.module.scss diff --git a/src/routes/index.tsx b/src/routes/index.tsx index c764f98..a7bab3d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -4,14 +4,15 @@ import { LandingPage } from '../pages/landing/LandingPage' import { Login } from '../pages/login' import { ProfilePage } from '../pages/profile' import { hexToNpub } from '../utils' -import { VerifyPage } from '../pages/verify' import { RelaysPage } from '../pages/relays' +import { SignPage } from '../pages/sign' export const appPrivateRoutes = { homePage: '/', create: '/create', verify: '/verify', - relays: '/relays' + relays: '/relays', + sign: '/sign' } export const appPublicRoutes = { @@ -50,12 +51,12 @@ export const privateRoutes = [ path: appPrivateRoutes.create, element: }, - { - path: appPrivateRoutes.verify, - element: - }, { path: appPrivateRoutes.relays, element: + }, + { + path: appPrivateRoutes.sign, + element: } ] diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 9a4158c..0b9f994 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -76,7 +76,7 @@ export const sendDM = async ( : 'You have received a signed document.' const decryptionUrl = `${window.location.origin}/#${ - appPrivateRoutes.verify + appPrivateRoutes.sign }?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent( encryptionKey )}` -- 2.34.1 From a70ef3371fca5cb0e3acb7f2812637391f920a57 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Wed, 15 May 2024 16:25:21 +0500 Subject: [PATCH 11/48] chore: decodeURIComponent of encryption key when entered manually --- src/pages/sign/index.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 632a885..6db8560 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -206,7 +206,10 @@ export const SignPage = () => { if (!selectedFile || !encryptionKey) return setIsLoading(true) - const arrayBuffer = await decrypt(selectedFile, encryptionKey) + const arrayBuffer = await decrypt( + selectedFile, + decodeURIComponent(encryptionKey) + ) if (!arrayBuffer) return @@ -482,11 +485,6 @@ export const SignPage = () => { placeholder="Select file" value={selectedFile} onChange={(value) => setSelectedFile(value)} - InputProps={{ - inputProps: { - accept: '.zip' - } - }} /> {selectedFile && ( -- 2.34.1 From 8ebeb7ef93663ce7d4a31f0865561ab6e01b7427 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:38:27 +0500 Subject: [PATCH 12/48] fix: In sign page, when doc is fully signed, update search params with update file url and key --- src/pages/sign/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 6db8560..fa0187b 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -51,7 +51,7 @@ enum SignedStatus { } export const SignPage = () => { - const [searchParams] = useSearchParams() + const [searchParams, setSearchParams] = useSearchParams() const [displayInput, setDisplayInput] = useState(false) @@ -123,7 +123,7 @@ export const SignPage = () => { const fileName = fileUrl.split('/').pop() const file = new File([res.data], fileName!) - decrypt(file, key).then((arrayBuffer) => { + decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => { if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) }) }) @@ -376,6 +376,14 @@ export const SignPage = () => { setAuthUrl ) } + + // when user is the last signer and has sent + // the final document to all the signers and viewers + // update search params with updated file url and encryption key + setSearchParams({ + file: fileUrl, + key: encryptionKey + }) } else { const nextSigner = meta.signers[signerIndex + 1] await sendDM( -- 2.34.1 From 144daaed4a7ae32bb93d5718e01faf7feac4c6d4 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:40:56 +0500 Subject: [PATCH 13/48] feat: add verify page --- src/pages/home/index.tsx | 6 + src/pages/verify/index.tsx | 332 +++++++++++++++++++++++++++++ src/pages/verify/style.module.scss | 39 ++++ src/routes/index.tsx | 5 + src/types/core.ts | 1 + 5 files changed, 383 insertions(+) create mode 100644 src/pages/verify/index.tsx create mode 100644 src/pages/verify/style.module.scss diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 1b46e58..d9647b3 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -20,6 +20,12 @@ export const HomePage = () => { > Sign + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx new file mode 100644 index 0000000..d808294 --- /dev/null +++ b/src/pages/verify/index.tsx @@ -0,0 +1,332 @@ +import { + Box, + Button, + List, + ListItem, + ListSubheader, + Typography, + useTheme +} from '@mui/material' +import JSZip from 'jszip' +import { MuiFileInput } from 'mui-file-input' +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { toast } from 'react-toastify' +import placeholderAvatar from '../../assets/images/nostr-logo.jpg' +import { LoadingSpinner } from '../../components/LoadingSpinner' +import { MetadataController } from '../../controllers' +import { getProfileRoute } from '../../routes' +import { Meta, ProfileMetadata } from '../../types' +import { + hexToNpub, + parseJson, + readContentOfZipEntry, + shorten +} from '../../utils' +import styles from './style.module.scss' +import { Event, verifyEvent } from 'nostr-tools' + +export const VerifyPage = () => { + const theme = useTheme() + + const textColor = theme.palette.getContrastText( + theme.palette.background.paper + ) + + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const [selectedFile, setSelectedFile] = useState(null) + const [meta, setMeta] = useState(null) + + const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + + useEffect(() => { + if (meta) { + const metadataController = new MetadataController() + + const users = [meta.submittedBy, ...meta.signers, ...meta.viewers] + + users.forEach((user) => { + if (!(user in metadata)) { + metadataController + .findMetadata(user) + .then((metadataEvent) => { + const metadataContent = + metadataController.extractProfileMetadataContent(metadataEvent) + if (metadataContent) + setMetadata((prev) => ({ + ...prev, + [user]: metadataContent + })) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user}`, + err + ) + }) + } + }) + } + }, [meta]) + + const handleVerify = async () => { + if (!selectedFile) return + setIsLoading(true) + + const zip = await JSZip.loadAsync(selectedFile).catch((err) => { + console.log('err in loading zip file :>> ', err) + toast.error(err.message || 'An error occurred in loading zip file.') + return null + }) + + if (!zip) return + + setLoadingSpinnerDesc('Parsing meta.json') + + const metaFileContent = await readContentOfZipEntry( + zip, + 'meta.json', + 'string' + ) + + if (!metaFileContent) { + setIsLoading(false) + return + } + + const parsedMetaJson = await parseJson(metaFileContent).catch( + (err) => { + console.log('err in parsing the content of meta.json :>> ', err) + toast.error( + err.message || 'error occurred in parsing the content of meta.json' + ) + setIsLoading(false) + return null + } + ) + + setMeta(parsedMetaJson) + setIsLoading(false) + } + + const imageLoadError = (event: any) => { + event.target.src = placeholderAvatar + } + + const getRoboImageUrl = (pubkey: string) => { + const npub = hexToNpub(pubkey) + return `https://robohash.org/${npub}.png?set=set3` + } + + const displayUser = (pubkey: string, verifySignature = false) => { + const profile = metadata[pubkey] + + let isValidSignature = false + + if (verifySignature) { + const signedEventString = meta ? meta.signedEvents[pubkey] : null + if (signedEventString) { + try { + const signedEvent = JSON.parse(signedEventString) + isValidSignature = verifyEvent(signedEvent) + } catch (error) { + console.error( + `An error occurred in parsing and verifying the signature event for ${pubkey}`, + error + ) + } + } + } + + return ( + + Profile Image + + + {profile?.display_name || + profile?.name || + shorten(hexToNpub(pubkey))} + + + {verifySignature && ( + + ({isValidSignature ? 'Valid' : 'Not Valid'} Signature) + + )} + + ) + } + + const displayExportedBy = () => { + if (!meta || !meta.exportSignature) return null + + const exportSignatureString = meta.exportSignature + + try { + const exportSignatureEvent = JSON.parse(exportSignatureString) as Event + + if (verifyEvent(exportSignatureEvent)) { + return displayUser(exportSignatureEvent.pubkey) + } else { + toast.error(`Invalid export signature!`) + return ( + + Invalid export signature + + ) + } + } catch (error) { + console.error(`An error occurred wile parsing exportSignature`, error) + return null + } + } + + return ( + <> + {isLoading && } + + {!meta && ( + <> + + Select exported zip file + + + setSelectedFile(value)} + InputProps={{ + inputProps: { + accept: '.zip' + } + }} + /> + + {selectedFile && ( + + + + )} + + )} + + {meta && ( + <> + + Meta Info + + } + > + + + Submitted By + + {displayUser(meta.submittedBy)} + + + + + Exported By + + {displayExportedBy()} + + + {meta.signers.length > 0 && ( + + + Signers + +
    + {meta.signers.map((signer) => ( +
  • + {displayUser(signer, true)} +
  • + ))} +
+
+ )} + + {meta.viewers.length > 0 && ( + + + Viewers + +
    + {meta.viewers.map((viewer) => ( +
  • + {displayUser(viewer)} +
  • + ))} +
+
+ )} + + + + Files + +
    + {Object.keys(meta.fileHashes).map((file, index) => ( +
  • + {file} +
  • + ))} +
+
+
+ + )} +
+ + ) +} diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss new file mode 100644 index 0000000..61fe63d --- /dev/null +++ b/src/pages/verify/style.module.scss @@ -0,0 +1,39 @@ +@import '../../colors.scss'; + +.container { + color: $text-color; + display: flex; + flex-direction: column; + + .subHeader { + border-bottom: 0.5px solid; + font-size: 1.5rem; + } + + .usersList { + display: flex; + flex-direction: column; + gap: 10px; + list-style: none; + margin-top: 10px; + } + + .user { + display: flex; + align-items: center; + gap: 10px; + + .name { + text-align: center; + cursor: pointer; + } + } + + .tableCell { + border-right: 1px solid rgba(224, 224, 224, 1); + + .user { + @extend .user; + } + } +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index a7bab3d..de51e1a 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -6,6 +6,7 @@ import { ProfilePage } from '../pages/profile' import { hexToNpub } from '../utils' import { RelaysPage } from '../pages/relays' import { SignPage } from '../pages/sign' +import { VerifyPage } from '../pages/verify' export const appPrivateRoutes = { homePage: '/', @@ -58,5 +59,9 @@ export const privateRoutes = [ { path: appPrivateRoutes.sign, element: + }, + { + path: appPrivateRoutes.verify, + element: } ] diff --git a/src/types/core.ts b/src/types/core.ts index 531e1fd..a328ac0 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -14,4 +14,5 @@ export interface Meta { fileHashes: { [key: string]: string } submittedBy: string signedEvents: { [key: string]: string } + exportSignature?: string } -- 2.34.1 From d600a94ebd78b4fda2409535444fcb1673d021ac Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:43:37 +0500 Subject: [PATCH 14/48] feat: In sign page navigate to verify after export --- src/pages/sign/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index fa0187b..56e6b72 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -21,11 +21,11 @@ import { MuiFileInput } from 'mui-file-input' import { EventTemplate } from 'nostr-tools' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { Link, useSearchParams } from 'react-router-dom' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' -import { getProfileRoute } from '../../routes' +import { appPrivateRoutes, getProfileRoute } from '../../routes' import { State } from '../../store/rootReducer' import { Meta, ProfileMetadata, User, UserRole } from '../../types' import { @@ -51,6 +51,7 @@ enum SignedStatus { } export const SignPage = () => { + const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const [displayInput, setDisplayInput] = useState(false) @@ -465,6 +466,8 @@ export const SignPage = () => { saveAs(blob, 'exported.zip') setIsLoading(false) + + navigate(appPrivateRoutes.verify) } if (authUrl) { -- 2.34.1 From 73b189560f93692f2d281e74cf4d4e01022d5e49 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:51:35 +0500 Subject: [PATCH 15/48] fix: handle navigation after create --- src/pages/create/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index c308026..64e40bd 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -20,10 +20,10 @@ import { } from '@mui/material' import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' -import { getProfileRoute } from '../../routes' +import { appPrivateRoutes, getProfileRoute } from '../../routes' import { ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, @@ -45,6 +45,7 @@ import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' export const CreatePage = () => { + const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -349,7 +350,7 @@ export const CreatePage = () => { } setIsLoading(false) - // todo: navigate to verify + navigate(`${appPrivateRoutes.sign}?file=${fileUrl}&key=${encryptionKey}`) } if (authUrl) { -- 2.34.1 From 5356262e34b6941e7d60ba1013ed918e4115360e Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:55:44 +0500 Subject: [PATCH 16/48] fix: handle the case when zip entry is undefined --- src/utils/zip.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 1d6379a..71bc556 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -17,6 +17,11 @@ export const readContentOfZipEntry = async ( // Get the zip entry corresponding to the specified file path const zipEntry = zip.files[filePath] + if (!zipEntry) { + toast.error(`Couldn't find file in zip archive at ${filePath}`) + return null + } + // Read the content of the zip entry asynchronously const fileContent = await zipEntry.async(outputType).catch((err) => { // Handle any errors that occur during the read operation -- 2.34.1 From be5661942b5433c364277ace2daa2be157747a72 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 11:04:19 +0500 Subject: [PATCH 17/48] chore: quick fix --- src/pages/create/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 64e40bd..2e175e2 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -350,7 +350,11 @@ export const CreatePage = () => { } setIsLoading(false) - navigate(`${appPrivateRoutes.sign}?file=${fileUrl}&key=${encryptionKey}`) + navigate( + `${appPrivateRoutes.sign}?file=${encodeURIComponent( + fileUrl + )}&key=${encodeURIComponent(encryptionKey)}` + ) } if (authUrl) { -- 2.34.1 From cbe4a683c745e546b331a57a7052675929767eb2 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 11:23:26 +0500 Subject: [PATCH 18/48] chore: merge main --- .env.example | 2 +- src/pages/home/index.tsx | 2 +- src/pages/landing/LandingPage.tsx | 20 +++++++++++--------- src/pages/login/index.tsx | 2 +- src/pages/profile/index.tsx | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 6b61434..7239454 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_MOST_POPULAR_RELAYS=wss://relay.damus.io wss://eden.nostr.land wss://nos.lol wss://relay.snort.social wss://relay.current.fyi wss://brb.io wss://nostr.orangepill.dev wss://nostr-pub.wellorder.net wss://nostr.bitcoiner.social wss://nostr.wine wss://nostr.oxtr.dev wss://relay.nostr.bg wss://nostr.mom wss://nostr.fmt.wiz.biz wss://relay.nostr.band wss://nostr-pub.semisol.dev wss://nostr.milou.lol wss://puravida.nostr.land wss://nostr.onsats.org wss://relay.nostr.info wss://offchain.pub wss://relay.orangepill.dev wss://no.str.cr wss://atlas.nostr.land wss://nostr.zebedee.cloud wss://nostr-relay.wlvs.space wss://relay.nostrati.com wss://relay.nostr.com.au wss://nostr.inosta.cc wss://nostr.rocks \ No newline at end of file +VITE_MOST_POPULAR_RELAYS=wss://relay.damus.io wss://eden.nostr.land wss://nos.lol wss://relay.snort.social wss://relay.current.fyi wss://brb.io wss://nostr.orangepill.dev wss://nostr-pub.wellorder.net wss://nostr.wine wss://nostr.oxtr.dev wss://relay.nostr.bg wss://nostr.mom wss://nostr.fmt.wiz.biz wss://relay.nostr.band wss://nostr-pub.semisol.dev wss://nostr.milou.lol wss://puravida.nostr.land wss://nostr.onsats.org wss://relay.nostr.info wss://offchain.pub wss://relay.orangepill.dev wss://no.str.cr wss://atlas.nostr.land wss://nostr.zebedee.cloud wss://nostr-relay.wlvs.space wss://relay.nostrati.com wss://relay.nostr.com.au wss://nostr.inosta.cc wss://nostr.rocks \ No newline at end of file diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index d9647b3..ff7a40f 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -22,7 +22,7 @@ export const HomePage = () => { diff --git a/src/pages/landing/LandingPage.tsx b/src/pages/landing/LandingPage.tsx index 9a24f04..948ab72 100644 --- a/src/pages/landing/LandingPage.tsx +++ b/src/pages/landing/LandingPage.tsx @@ -59,7 +59,7 @@ export const LandingPage = () => { }} variant="h4" > - What is Nostr? + What is SIGit? { }} variant="body1" > - Nostr is a decentralised messaging protocol where YOU own your - identity. To get started, you must have an existing{' '} + SIGit is an open-source and self-hostable solution for secure + document signing and verification. Code is MIT licenced and + available at{' '} - Nostr account + https://git.sigit.io/sig/it .

- No email required - all notifications are made using the nQuiz - relay. + SIGit lets you Create, Sign and Verify signature packs from any + device with a browser.

- If you no longer wish to hear from us, simply remove - relay.nquiz.io from your list of relays. + Unlike other solutions, SIGit is totally private - files are + encrypted locally, and valid packs can only be exported by named + recipients.
diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index bc1366a..24ed7ca 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -303,7 +303,7 @@ export const Login = () => {
Welcome to Sigit setInputValue(e.target.value)} sx={{ width: '100%', mt: 2 }} diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index e57e9f7..12700f0 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -288,7 +288,7 @@ export const ProfilePage = () => { setAvatarLoading(false) }} className={styles.img} - src={profileMetadata.picture || npub ? getRoboHashPicture(npub!) : ''} + src={profileMetadata.picture || placeholderAvatar} alt="Profile Image" /> -- 2.34.1 From 1d59c666117927ae5e07e03e47af7fa0b5384722 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 11:25:30 +0500 Subject: [PATCH 19/48] chore: formatter fix --- src/pages/verify/index.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index d808294..6ce6af8 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -147,8 +147,8 @@ export const VerifyPage = () => { Profile Image { }} /> - + {profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))} {verifySignature && ( - + ({isValidSignature ? 'Valid' : 'Not Valid'} Signature) )} @@ -184,7 +184,7 @@ export const VerifyPage = () => { } else { toast.error(`Invalid export signature!`) return ( - + Invalid export signature ) @@ -201,12 +201,12 @@ export const VerifyPage = () => { {!meta && ( <> - + Select exported zip file setSelectedFile(value)} InputProps={{ @@ -218,7 +218,7 @@ export const VerifyPage = () => { {selectedFile && ( - @@ -245,7 +245,7 @@ export const VerifyPage = () => { gap: '15px' }} > - + Submitted By {displayUser(meta.submittedBy)} @@ -257,7 +257,7 @@ export const VerifyPage = () => { gap: '15px' }} > - + Exported By {displayExportedBy()} @@ -271,7 +271,7 @@ export const VerifyPage = () => { alignItems: 'flex-start' }} > - + Signers
    @@ -292,7 +292,7 @@ export const VerifyPage = () => { alignItems: 'flex-start' }} > - + Viewers
      @@ -312,7 +312,7 @@ export const VerifyPage = () => { alignItems: 'flex-start' }} > - + Files
        -- 2.34.1 From dd0e6bd7e489c30bf560ac18c4c80efab4283f3b Mon Sep 17 00:00:00 2001 From: ^ Date: Wed, 15 May 2024 18:48:28 +0100 Subject: [PATCH 20/48] fix: landing page --- src/pages/landing/LandingPage.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/landing/LandingPage.tsx b/src/pages/landing/LandingPage.tsx index 948ab72..4f076ea 100644 --- a/src/pages/landing/LandingPage.tsx +++ b/src/pages/landing/LandingPage.tsx @@ -71,24 +71,23 @@ export const LandingPage = () => { > SIGit is an open-source and self-hostable solution for secure document signing and verification. Code is MIT licenced and - available at{' '} + available at - https://git.sigit.io/sig/it + git.sigit.io/sig/it .

        - SIGit lets you Create, Sign and Verify signature packs from any + Multi-device - Create, Sign and Verify signature packs from any device with a browser.

        - Unlike other solutions, SIGit is totally private - files are - encrypted locally, and valid packs can only be exported by named - recipients. + Secure and Private - Documents encrypted at each step, content can + only be decrypted by named recipients. -- 2.34.1 From aa3faba271ae2f5b079ed4babdf91781dae5a5a9 Mon Sep 17 00:00:00 2001 From: ^ Date: Wed, 15 May 2024 19:28:27 +0100 Subject: [PATCH 21/48] fix: landing page wording --- src/pages/landing/LandingPage.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/landing/LandingPage.tsx b/src/pages/landing/LandingPage.tsx index 4f076ea..948ab72 100644 --- a/src/pages/landing/LandingPage.tsx +++ b/src/pages/landing/LandingPage.tsx @@ -71,23 +71,24 @@ export const LandingPage = () => { > SIGit is an open-source and self-hostable solution for secure document signing and verification. Code is MIT licenced and - available at + available at{' '} - git.sigit.io/sig/it + https://git.sigit.io/sig/it .

        - Multi-device - Create, Sign and Verify signature packs from any + SIGit lets you Create, Sign and Verify signature packs from any device with a browser.

        - Secure and Private - Documents encrypted at each step, content can - only be decrypted by named recipients. + Unlike other solutions, SIGit is totally private - files are + encrypted locally, and valid packs can only be exported by named + recipients. -- 2.34.1 From 628113168b3dc437bab63015c0dca66a57321a6e Mon Sep 17 00:00:00 2001 From: ^ Date: Wed, 15 May 2024 23:44:21 +0100 Subject: [PATCH 22/48] chore: input box label --- src/pages/login/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 24ed7ca..13217ed 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -303,7 +303,7 @@ export const Login = () => {
        Welcome to Sigit setInputValue(e.target.value)} sx={{ width: '100%', mt: 2 }} -- 2.34.1 From f0137d83788a6af75d3dfe8a9c46a0da85138557 Mon Sep 17 00:00:00 2001 From: ^ Date: Wed, 15 May 2024 23:54:20 +0100 Subject: [PATCH 23/48] fix: label --- src/pages/login/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 13217ed..24ed7ca 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -303,7 +303,7 @@ export const Login = () => {
        Welcome to Sigit setInputValue(e.target.value)} sx={{ width: '100%', mt: 2 }} -- 2.34.1 From f80df772f8e9207219db94fd24c343b09737b191 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 10:26:30 +0200 Subject: [PATCH 24/48] fix: verify page robohash --- src/pages/verify/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6ce6af8..854f4df 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -18,6 +18,7 @@ import { MetadataController } from '../../controllers' import { getProfileRoute } from '../../routes' import { Meta, ProfileMetadata } from '../../types' import { + getRoboHashPicture, hexToNpub, parseJson, readContentOfZipEntry, @@ -113,8 +114,9 @@ export const VerifyPage = () => { setIsLoading(false) } - const imageLoadError = (event: any) => { - event.target.src = placeholderAvatar + const imageLoadError = (event: any, pubkey: string) => { + const npub = hexToNpub(pubkey) + event.target.src = npub } const getRoboImageUrl = (pubkey: string) => { @@ -145,7 +147,7 @@ export const VerifyPage = () => { return ( {imageLoadError(event, pubkey)}} src={profile?.picture || getRoboImageUrl(pubkey)} alt="Profile Image" className="profile-image" -- 2.34.1 From 7db7e5de9e96b4764c1468c0dad373cc9595f4d6 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 10:56:08 +0200 Subject: [PATCH 25/48] fix: reverting signing of nostr auth token --- src/controllers/AuthController.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index bdb4410..1e853d1 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -45,6 +45,20 @@ export class AuthController { console.error('Error occurred while finding metadata', err) }) + // Nostr uses unix timestamps + const timestamp = Math.floor(Date.now() / 1000) + const { hostname } = window.location + + const authEvent: EventTemplate = { + kind: 1, + tags: [], + content: `${hostname}-${timestamp}`, + created_at: timestamp + } + + const signedAuthEvent = await this.nostrController.signEvent(authEvent) + this.createAndSaveAuthToken(signedAuthEvent) + store.dispatch( setAuthState({ loggedIn: true, -- 2.34.1 From 717cfc49ab236196472d3541eb56fb475bac908c Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 11:43:17 +0200 Subject: [PATCH 26/48] chore: removed unused import hexToNpub --- src/components/AppBar/AppBar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index eef58b7..9fa5491 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -25,7 +25,6 @@ import { import { clearAuthToken, getRoboHashPicture, - hexToNpub, saveNsecBunkerDelegatedKey, shorten } from '../../utils' -- 2.34.1 From 1f0faef6f6d709c4548f98d0d6f82ea5274127a2 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 16:45:00 +0200 Subject: [PATCH 27/48] fix: nsec login, metadata overlapping, robohash image in metadata state --- package-lock.json | 62 +++++++++++++++++++++++++-- package.json | 1 + src/components/AppBar/AppBar.tsx | 32 +++++++++----- src/controllers/AuthController.ts | 11 ++++- src/controllers/MetadataController.ts | 12 ++++++ src/controllers/NostrController.ts | 9 ++-- src/layouts/Main.tsx | 14 ++++-- src/pages/create/index.tsx | 5 +++ src/pages/profile/index.tsx | 6 +-- src/pages/verify/index.tsx | 2 +- src/utils/localStorage.ts | 4 ++ 11 files changed, 131 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1556fa..f5270b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mui/icons-material": "5.15.11", "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", + "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "2.5.0", "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", @@ -1505,9 +1506,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -1598,6 +1599,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", @@ -1863,6 +1875,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", @@ -1875,6 +1909,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -4027,6 +4072,17 @@ } } }, + "node_modules/nostr-tools/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/nostr-wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", diff --git a/package.json b/package.json index 75a6c85..0eca093 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@mui/icons-material": "5.15.11", "@mui/lab": "5.0.0-alpha.166", "@mui/material": "5.15.11", + "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "2.5.0", "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 9fa5491..b68ceb2 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -10,26 +10,28 @@ import { import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { setAuthState } from '../../store/actions' +import { setAuthState, setMetadataEvent } from '../../store/actions' import { State } from '../../store/rootReducer' import { Dispatch } from '../../store/store' import Username from '../username' import { Link, useNavigate } from 'react-router-dom' -import { NostrController } from '../../controllers' import { appPrivateRoutes, appPublicRoutes, getProfileRoute } from '../../routes' +import { MetadataController, NostrController } from '../../controllers' import { clearAuthToken, - getRoboHashPicture, + clearState, saveNsecBunkerDelegatedKey, shorten } from '../../utils' import styles from './style.module.scss' +const metadataController = new MetadataController() + export const AppBar = () => { const navigate = useNavigate() @@ -43,17 +45,21 @@ export const AppBar = () => { const metadataState = useSelector((state: State) => state.metadata) useEffect(() => { - if (metadataState && metadataState.content) { - const { picture, display_name, name } = JSON.parse(metadataState.content) - const pubkey = authState?.usersPubkey || '' + if (metadataState) { + if (metadataState.content) { + const { picture, display_name, name } = JSON.parse( + metadataState.content + ) - if (picture) { - setUserAvatar(picture) + if (picture) { + setUserAvatar(picture) + } + + setUsername(shorten(display_name || name || '', 7)) } else { - setUserAvatar(getRoboHashPicture(pubkey)) + setUserAvatar('') + setUsername('') } - - setUsername(shorten(display_name || name || '', 7)) } }, [metadataState]) @@ -76,6 +82,7 @@ export const AppBar = () => { handleCloseUserMenu() dispatch( setAuthState({ + keyPair: undefined, loggedIn: false, usersPubkey: undefined, loginMethod: undefined, @@ -83,8 +90,11 @@ export const AppBar = () => { }) ) + dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent())) + // clear authToken saved in local storage clearAuthToken() + clearState() // update nsecBunker delegated key after logout const nostrController = NostrController.getInstance() diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 1e853d1..82c13ba 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -10,6 +10,7 @@ import { base64DecodeAuthToken, base64EncodeSignedEvent, getAuthToken, + getRoboHashPicture, getVisitedLink, saveAuthToken, compareObjects @@ -42,7 +43,15 @@ export class AuthController { store.dispatch(setMetadataEvent(event)) }) .catch((err) => { - console.error('Error occurred while finding metadata', err) + console.warn('Error occurred while finding metadata', err) + + const emptyMetadata = this.metadataController.getEmptyMetadataEvent() + + emptyMetadata.content = JSON.stringify({ + picture: getRoboHashPicture(pubkey) + }) + + store.dispatch(setMetadataEvent(emptyMetadata)) }) // Nostr uses unix timestamps diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index a2a2e5d..f13440d 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -23,6 +23,18 @@ export class MetadataController { this.nostrController = NostrController.getInstance() } + public getEmptyMetadataEvent = (): Event => { + return { + content: '', + created_at: new Date().valueOf(), + id: '', + kind: 0, + pubkey: '', + sig: '', + tags: [] + } + } + public findMetadata = async (hexKey: string) => { const eventFilter: Filter = { kinds: [kinds.Metadata], diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index f67e7f4..cd433f5 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -326,15 +326,18 @@ export class NostrController extends EventEmitter { } if (loginMethod === LoginMethods.privateKey) { - const keyPair = (store.getState().auth as AuthState).keyPair + const keys = (store.getState().auth as AuthState).keyPair - if (!keyPair) { + if (!keys) { throw new Error( `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` ) } - const encrypted = await nip04.encrypt(keyPair.private, receiver, content) + const { private: nsec } = keys + const privateKey = nip19.decode(nsec).data as Uint8Array + + const encrypted = await nip04.encrypt(privateKey, receiver, content) return encrypted } diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index b452354..e258e36 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -4,13 +4,15 @@ import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { Outlet } from 'react-router-dom' import { AppBar } from '../components/AppBar/AppBar' -import { restoreState, setAuthState } from '../store/actions' -import { clearAuthToken, loadState, saveNsecBunkerDelegatedKey } from '../utils' +import { restoreState, setAuthState, setMetadataEvent } from '../store/actions' +import { clearAuthToken, clearState, loadState, saveNsecBunkerDelegatedKey } from '../utils' import { LoadingSpinner } from '../components/LoadingSpinner' import { Dispatch } from '../store/store' -import { NostrController } from '../controllers' +import { MetadataController, NostrController } from '../controllers' import { LoginMethods } from '../store/auth/types' +const metadataController = new MetadataController() + export const MainLayout = () => { const dispatch: Dispatch = useDispatch() const [isLoading, setIsLoading] = useState(true) @@ -19,6 +21,7 @@ export const MainLayout = () => { const logout = () => { dispatch( setAuthState({ + keyPair: undefined, loggedIn: false, usersPubkey: undefined, loginMethod: undefined, @@ -26,8 +29,13 @@ export const MainLayout = () => { }) ) + dispatch( + setMetadataEvent(metadataController.getEmptyMetadataEvent()) + ) + // clear authToken saved in local storage clearAuthToken() + clearState() // update nsecBunker delegated key const newDelegatedKey = diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 2e175e2..6db90a3 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -489,6 +489,11 @@ const DisplayUser = ({ }) }, [users]) + /** + * Use robohash if any of the users images fail to load + * @param event img tag onError event + * @param pubkey of the user + */ const imageLoadError = (event: any, pubkey: string) => { event.target.src = getRoboHashPicture(pubkey) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 12700f0..ca6fbd5 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -26,7 +26,6 @@ import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' import { SmartToy } from '@mui/icons-material' -import { getRoboHashPicture } from '../../utils' export const ProfilePage = () => { const theme = useTheme() @@ -281,14 +280,11 @@ export const ProfilePage = () => { }} > { - event.target.src = npub ? getRoboHashPicture(npub) : '' - }} onLoad={() => { setAvatarLoading(false) }} className={styles.img} - src={profileMetadata.picture || placeholderAvatar} + src={profileMetadata.picture} alt="Profile Image" /> diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 854f4df..cd77b27 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -116,7 +116,7 @@ export const VerifyPage = () => { const imageLoadError = (event: any, pubkey: string) => { const npub = hexToNpub(pubkey) - event.target.src = npub + event.target.src = getRoboImageUrl(npub) } const getRoboImageUrl = (pubkey: string) => { diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts index 15c17db..92b2db2 100644 --- a/src/utils/localStorage.ts +++ b/src/utils/localStorage.ts @@ -22,6 +22,10 @@ export const loadState = (): State | undefined => { } } +export const clearState = () => { + localStorage.removeItem('state') +} + export const saveNsecBunkerDelegatedKey = (privateKey: string) => { localStorage.setItem('nsecbunker-delegated-key', privateKey) } -- 2.34.1 From b2a419adbf73f37873c2145a1c466ce76c23e8f5 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 16 May 2024 17:15:21 +0200 Subject: [PATCH 28/48] fix: robohash image missing with NIP05 login --- src/controllers/AuthController.ts | 38 +++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 82c13ba..a43a3d7 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -37,20 +37,44 @@ export class AuthController { * or error if otherwise */ async authAndGetMetadataAndRelaysMap(pubkey: string) { + const emptyMetadata = this.metadataController.getEmptyMetadataEvent() + + emptyMetadata.content = JSON.stringify({ + picture: getRoboHashPicture(pubkey) + }) + this.metadataController .findMetadata(pubkey) .then((event) => { - store.dispatch(setMetadataEvent(event)) + if (event) { + // In case of NIP05 there is scenario where login content will be populated but without an image + // In such case we will add robohash image + if (event.content) { + const content = JSON.parse(event.content) + + if (!content) { + event.content = '' + } + + if (!content.picture) { + content.picture = getRoboHashPicture(pubkey) + } + + event.content = JSON.stringify(content) + } else { + event.content = JSON.stringify({ + picture: getRoboHashPicture(pubkey) + }) + } + + store.dispatch(setMetadataEvent(event)) + } else { + store.dispatch(setMetadataEvent(emptyMetadata)) + } }) .catch((err) => { console.warn('Error occurred while finding metadata', err) - const emptyMetadata = this.metadataController.getEmptyMetadataEvent() - - emptyMetadata.content = JSON.stringify({ - picture: getRoboHashPicture(pubkey) - }) - store.dispatch(setMetadataEvent(emptyMetadata)) }) -- 2.34.1 From ae213b796b24d444c1b15bf61b390f9fdccc2d9a Mon Sep 17 00:00:00 2001 From: Davinci Date: Fri, 17 May 2024 09:37:30 +0200 Subject: [PATCH 29/48] fix: looping trough robo sets, image not shown when visiting profile while not logged in --- .env.example | 2 +- src/pages/profile/index.tsx | 17 ++++++++++++----- src/utils/nostr.ts | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 7239454..6b61434 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_MOST_POPULAR_RELAYS=wss://relay.damus.io wss://eden.nostr.land wss://nos.lol wss://relay.snort.social wss://relay.current.fyi wss://brb.io wss://nostr.orangepill.dev wss://nostr-pub.wellorder.net wss://nostr.wine wss://nostr.oxtr.dev wss://relay.nostr.bg wss://nostr.mom wss://nostr.fmt.wiz.biz wss://relay.nostr.band wss://nostr-pub.semisol.dev wss://nostr.milou.lol wss://puravida.nostr.land wss://nostr.onsats.org wss://relay.nostr.info wss://offchain.pub wss://relay.orangepill.dev wss://no.str.cr wss://atlas.nostr.land wss://nostr.zebedee.cloud wss://nostr-relay.wlvs.space wss://relay.nostrati.com wss://relay.nostr.com.au wss://nostr.inosta.cc wss://nostr.rocks \ No newline at end of file +VITE_MOST_POPULAR_RELAYS=wss://relay.damus.io wss://eden.nostr.land wss://nos.lol wss://relay.snort.social wss://relay.current.fyi wss://brb.io wss://nostr.orangepill.dev wss://nostr-pub.wellorder.net wss://nostr.bitcoiner.social wss://nostr.wine wss://nostr.oxtr.dev wss://relay.nostr.bg wss://nostr.mom wss://nostr.fmt.wiz.biz wss://relay.nostr.band wss://nostr-pub.semisol.dev wss://nostr.milou.lol wss://puravida.nostr.land wss://nostr.onsats.org wss://relay.nostr.info wss://offchain.pub wss://relay.orangepill.dev wss://no.str.cr wss://atlas.nostr.land wss://nostr.zebedee.cloud wss://nostr-relay.wlvs.space wss://relay.nostrati.com wss://relay.nostr.com.au wss://nostr.inosta.cc wss://nostr.rocks \ No newline at end of file diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index ca6fbd5..677bbe7 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -12,7 +12,7 @@ import { useTheme } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { MetadataController, NostrController } from '../../controllers' @@ -26,6 +26,7 @@ import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' import { SmartToy } from '@mui/icons-material' +import { getRoboHashPicture } from '../../utils' export const ProfilePage = () => { const theme = useTheme() @@ -52,6 +53,8 @@ export const ProfilePage = () => { const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc] = useState('Fetching metadata') + const robotSet = useRef(1) + useEffect(() => { if (npub) { try { @@ -213,7 +216,10 @@ export const ProfilePage = () => { const generateRobotAvatar = () => { setAvatarLoading(true) - const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3` + robotSet.current++ + if (robotSet.current > 5) robotSet.current = 1 + + const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current) setProfileMetadata((prev) => ({ ...prev, @@ -233,8 +239,6 @@ export const ProfilePage = () => { * @returns robohash generate button, loading spinner or no button */ const robohashButton = () => { - if (profileMetadata?.picture?.includes('robohash')) return null - return ( {avatarLoading ? ( @@ -280,6 +284,9 @@ export const ProfilePage = () => { }} > { + event.target.src = npub ? getRoboHashPicture(npub) : '' + }} onLoad={() => { setAvatarLoading(false) }} @@ -305,7 +312,7 @@ export const ProfilePage = () => { {editItem('picture', 'Picture URL', undefined, undefined, { - endAdornment: robohashButton() + endAdornment: isUsersOwnProfile ? robohashButton() : undefined })} {editItem('name', 'Username')} diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index a39502f..7d5b23d 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -145,7 +145,7 @@ export const base64DecodeAuthToken = (authToken: string): SignedEvent => { * @param pubkey in hex or npub format * @returns robohash.org url for the avatar */ -export const getRoboHashPicture = (pubkey: string): string => { +export const getRoboHashPicture = (pubkey: string, set: number = 1): string => { const npub = hexToNpub(pubkey) - return `https://robohash.org/${npub}.png?set=set3` + return `https://robohash.org/${npub}.png?set=set${set}` } -- 2.34.1 From 95621175910b2a5ae3a6f37be675a9ae6eaba724 Mon Sep 17 00:00:00 2001 From: Yury Date: Fri, 17 May 2024 08:51:28 +0300 Subject: [PATCH 30/48] fix(DM): removed direct download link --- src/utils/misc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 0b9f994..521b6b5 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -81,7 +81,7 @@ export const sendDM = async ( encryptionKey )}` - const content = `${initialLine}\n\n${decryptionUrl}\n\nDirect download${fileUrl}` + const content = `${initialLine}\n\n${decryptionUrl}` // Set up event listener for authentication event nostrController.on('nsecbunker-auth', (url) => { -- 2.34.1 From b516c53a62818bc37ab9ee48d3f7f7ba9f772e77 Mon Sep 17 00:00:00 2001 From: Davinci Date: Fri, 17 May 2024 13:33:01 +0200 Subject: [PATCH 31/48] fix: profile picture inconsistencies, login with enter --- src/components/AppBar/AppBar.tsx | 12 ++++--- src/controllers/AuthController.ts | 24 ------------- src/controllers/MetadataController.ts | 1 + src/layouts/Main.tsx | 24 +++++++++++-- src/main.tsx | 3 +- src/pages/login/index.tsx | 11 ++++++ src/pages/profile/index.tsx | 52 ++++++++++++++------------- src/store/actionTypes.ts | 2 ++ src/store/rootReducer.ts | 5 ++- src/store/userRobotImage/action.ts | 7 ++++ src/store/userRobotImage/reducer.ts | 22 ++++++++++++ src/store/userRobotImage/types.ts | 9 +++++ src/utils/nostr.ts | 3 +- 13 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 src/store/userRobotImage/action.ts create mode 100644 src/store/userRobotImage/reducer.ts create mode 100644 src/store/userRobotImage/types.ts diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index b68ceb2..a1971ed 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -29,6 +29,7 @@ import { shorten } from '../../utils' import styles from './style.module.scss' +import { setUserRobotImage } from '../../store/userRobotImage/action' const metadataController = new MetadataController() @@ -43,6 +44,7 @@ export const AppBar = () => { const authState = useSelector((state: State) => state.auth) const metadataState = useSelector((state: State) => state.metadata) + const userRobotImage = useSelector((state: State) => state.userRobotImage) useEffect(() => { if (metadataState) { @@ -51,17 +53,17 @@ export const AppBar = () => { metadataState.content ) - if (picture) { - setUserAvatar(picture) + if (picture || userRobotImage) { + setUserAvatar(picture || userRobotImage) } setUsername(shorten(display_name || name || '', 7)) } else { - setUserAvatar('') + setUserAvatar(userRobotImage || '') setUsername('') } } - }, [metadataState]) + }, [metadataState, userRobotImage]) const handleOpenUserMenu = (event: React.MouseEvent) => { setAnchorElUser(event.currentTarget) @@ -89,8 +91,8 @@ export const AppBar = () => { nsecBunkerPubkey: undefined }) ) - dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent())) + dispatch(setUserRobotImage(null)) // clear authToken saved in local storage clearAuthToken() diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index a43a3d7..01c5280 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -39,34 +39,10 @@ export class AuthController { async authAndGetMetadataAndRelaysMap(pubkey: string) { const emptyMetadata = this.metadataController.getEmptyMetadataEvent() - emptyMetadata.content = JSON.stringify({ - picture: getRoboHashPicture(pubkey) - }) - this.metadataController .findMetadata(pubkey) .then((event) => { if (event) { - // In case of NIP05 there is scenario where login content will be populated but without an image - // In such case we will add robohash image - if (event.content) { - const content = JSON.parse(event.content) - - if (!content) { - event.content = '' - } - - if (!content.picture) { - content.picture = getRoboHashPicture(pubkey) - } - - event.content = JSON.stringify(content) - } else { - event.content = JSON.stringify({ - picture: getRoboHashPicture(pubkey) - }) - } - store.dispatch(setMetadataEvent(event)) } else { store.dispatch(setMetadataEvent(emptyMetadata)) diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index f13440d..360acc6 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -142,6 +142,7 @@ export class MetadataController { public extractProfileMetadataContent = (event: VerifiedEvent) => { try { + if (!event.content) return {} return JSON.parse(event.content) as ProfileMetadata } catch (error) { console.log('error in parsing metadata event content :>> ', error) diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index e258e36..c1a827e 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -1,21 +1,24 @@ import { Box } from '@mui/material' import Container from '@mui/material/Container' import { useEffect, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { Outlet } from 'react-router-dom' import { AppBar } from '../components/AppBar/AppBar' import { restoreState, setAuthState, setMetadataEvent } from '../store/actions' -import { clearAuthToken, clearState, loadState, saveNsecBunkerDelegatedKey } from '../utils' +import { clearAuthToken, clearState, getRoboHashPicture, loadState, saveNsecBunkerDelegatedKey } from '../utils' import { LoadingSpinner } from '../components/LoadingSpinner' import { Dispatch } from '../store/store' import { MetadataController, NostrController } from '../controllers' import { LoginMethods } from '../store/auth/types' +import { setUserRobotImage } from '../store/userRobotImage/action' +import { State } from '../store/rootReducer' const metadataController = new MetadataController() export const MainLayout = () => { const dispatch: Dispatch = useDispatch() const [isLoading, setIsLoading] = useState(true) + const authState = useSelector((state: State) => state.auth) useEffect(() => { const logout = () => { @@ -67,6 +70,23 @@ export const MainLayout = () => { setIsLoading(false) }, [dispatch]) + /** + * When authState change user logged in / or app reloaded + * we set robohash avatar in the global state based on user npub + * so that avatar will be consistent across the app when kind 0 is empty + */ + useEffect(() => { + if (authState && authState.loggedIn) { + const pubkey = authState.usersPubkey || authState.keyPair?.public + + if (pubkey) { + dispatch( + setUserRobotImage(getRoboHashPicture(pubkey)) + ) + } + } + }, [authState]) + if (isLoading) return return ( diff --git a/src/main.tsx b/src/main.tsx index 4da2ffd..202d747 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,7 +15,8 @@ store.subscribe( saveState({ auth: store.getState().auth, metadata: store.getState().metadata, - relays: store.getState().relays + relays: store.getState().relays, + userRobotImage: store.getState().userRobotImage }) }, 1000) ) diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 24ed7ca..9d14729 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -43,6 +43,16 @@ export const Login = () => { }, 500) }, []) + /** + * Call login function when enter is pressed + */ + const handleInputKeyDown = (event: any) => { + if (event.code === "Enter" || event.code === "NumpadEnter") { + event.preventDefault(); + login() + } + } + const loginWithExtension = async () => { setIsLoading(true) setLoadingSpinnerDesc('Capturing pubkey from nostr extension') @@ -303,6 +313,7 @@ export const Login = () => {
        Welcome to Sigit setInputValue(e.target.value)} diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 677bbe7..bc6d587 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,6 +1,5 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { - CircularProgress, IconButton, InputProps, List, @@ -43,10 +42,10 @@ export const ProfilePage = () => { useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) - const [avatarLoading, setAvatarLoading] = useState(false) const metadataState = useSelector((state: State) => state.metadata) const keys = useSelector((state: State) => state.auth?.keyPair) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) + const userRobotImage = useSelector((state: State) => state.userRobotImage) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) @@ -213,9 +212,12 @@ export const ProfilePage = () => { setSavingProfileMetadata(false) } + /** + * Called by clicking on the robot icon inside Picture URL input + * On every click, next robohash set will be generated. + * There are 5 sets at the moment, after 5th set function will start over from set 1. + */ const generateRobotAvatar = () => { - setAvatarLoading(true) - robotSet.current++ if (robotSet.current > 5) robotSet.current = 1 @@ -223,15 +225,8 @@ export const ProfilePage = () => { setProfileMetadata((prev) => ({ ...prev, - picture: '' + picture: robotAvatarLink })) - - setTimeout(() => { - setProfileMetadata((prev) => ({ - ...prev, - picture: robotAvatarLink - })) - }) } /** @@ -241,17 +236,29 @@ export const ProfilePage = () => { const robohashButton = () => { return ( - {avatarLoading ? ( - - ) : ( - - - - )} + + + ) } + /** + * Handles the logic for Image URL. + * If no picture in kind 0 found - use robohash avatar + * + * @returns robohash image url + */ + const getProfileImage = (metadata: ProfileMetadata) => { + if (!isUsersOwnProfile) { + return metadata.picture || getRoboHashPicture(npub!) + } + + // userRobotImage is used only when visiting own profile + // while kind 0 picture is not set + return metadata.picture || userRobotImage || getRoboHashPicture(npub!) + } + return ( <> {isLoading && } @@ -285,13 +292,10 @@ export const ProfilePage = () => { > { - event.target.src = npub ? getRoboHashPicture(npub) : '' + event.target.src = getRoboHashPicture(npub!) }} - onLoad={() => { - setAvatarLoading(false) - }} className={styles.img} - src={profileMetadata.picture} + src={getProfileImage(profileMetadata)} alt="Profile Image" /> diff --git a/src/store/actionTypes.ts b/src/store/actionTypes.ts index 524e81f..65b037b 100644 --- a/src/store/actionTypes.ts +++ b/src/store/actionTypes.ts @@ -9,3 +9,5 @@ export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS' export const SET_METADATA_EVENT = 'SET_METADATA_EVENT' export const SET_RELAY_MAP = 'SET_RELAY_MAP' + +export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE' diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 7857e4a..7ad7770 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -5,15 +5,18 @@ import { AuthState } from './auth/types' import metadataReducer from './metadata/reducer' import { RelaysState } from './relays/types' import relaysReducer from './relays/reducer' +import userRobotImageReducer from './userRobotImage/reducer' export interface State { auth: AuthState relays: RelaysState metadata?: Event + userRobotImage?: string } export default combineReducers({ auth: authReducer, metadata: metadataReducer, - relays: relaysReducer + relays: relaysReducer, + userRobotImage: userRobotImageReducer }) diff --git a/src/store/userRobotImage/action.ts b/src/store/userRobotImage/action.ts new file mode 100644 index 0000000..cfb83e6 --- /dev/null +++ b/src/store/userRobotImage/action.ts @@ -0,0 +1,7 @@ +import * as ActionTypes from '../actionTypes' +import { SetUserRobotImage } from './types' + +export const setUserRobotImage = (payload: string | null): SetUserRobotImage => ({ + type: ActionTypes.SET_USER_ROBOT_IMAGE, + payload +}) diff --git a/src/store/userRobotImage/reducer.ts b/src/store/userRobotImage/reducer.ts new file mode 100644 index 0000000..61369ed --- /dev/null +++ b/src/store/userRobotImage/reducer.ts @@ -0,0 +1,22 @@ +import * as ActionTypes from '../actionTypes' +import { MetadataDispatchTypes } from './types' + +const initialState: string | null = null + +const reducer = ( + state = initialState, + action: MetadataDispatchTypes +): string | null => { + switch (action.type) { + case ActionTypes.SET_USER_ROBOT_IMAGE: + return action.payload + + case ActionTypes.RESTORE_STATE: + return action.payload.userRobotImage || null + + default: + return state + } +} + +export default reducer diff --git a/src/store/userRobotImage/types.ts b/src/store/userRobotImage/types.ts new file mode 100644 index 0000000..05d1475 --- /dev/null +++ b/src/store/userRobotImage/types.ts @@ -0,0 +1,9 @@ +import * as ActionTypes from '../actionTypes' +import { RestoreState } from '../actions' + +export interface SetUserRobotImage { + type: typeof ActionTypes.SET_USER_ROBOT_IMAGE + payload: string | null +} + +export type MetadataDispatchTypes = SetUserRobotImage | RestoreState diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index 7d5b23d..ad68486 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -145,7 +145,8 @@ export const base64DecodeAuthToken = (authToken: string): SignedEvent => { * @param pubkey in hex or npub format * @returns robohash.org url for the avatar */ -export const getRoboHashPicture = (pubkey: string, set: number = 1): string => { +export const getRoboHashPicture = (pubkey?: string, set: number = 1): string => { + if (!pubkey) return '' const npub = hexToNpub(pubkey) return `https://robohash.org/${npub}.png?set=set${set}` } -- 2.34.1 From 2d06e21bf550ae92449cce00c5ee66cd51960f33 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 16:22:05 +0500 Subject: [PATCH 32/48] chore create a component for user --- src/components/username.tsx | 45 ++++++++++++++++++++- src/pages/create/index.tsx | 56 ++++++++------------------ src/pages/sign/index.tsx | 79 +++++++++++-------------------------- src/pages/verify/index.tsx | 55 ++++++++++---------------- 4 files changed, 102 insertions(+), 133 deletions(-) diff --git a/src/components/username.tsx b/src/components/username.tsx index 928d0c6..387a4c6 100644 --- a/src/components/username.tsx +++ b/src/components/username.tsx @@ -1,6 +1,9 @@ -import { Typography, IconButton } from '@mui/material' +import { Typography, IconButton, Box, useTheme } from '@mui/material' import { useSelector } from 'react-redux' import { State } from '../store/rootReducer' +import { hexToNpub } from '../utils' +import { Link } from 'react-router-dom' +import { getProfileRoute } from '../routes' type Props = { username: string @@ -44,3 +47,43 @@ const Username = ({ username, avatarContent, handleClick }: Props) => { } export default Username + +type UserProps = { + pubkey: string + name: string + image?: string +} + +export const UserComponent = ({ pubkey, name, image }: UserProps) => { + const theme = useTheme() + + const npub = hexToNpub(pubkey) + const roboImage = `https://robohash.org/${npub}.png?set=set3` + + return ( + + User Image + + + {name} + + + + ) +} diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 6db90a3..2f9fc67 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -18,18 +18,22 @@ import { Tooltip, Typography } from '@mui/material' +import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { useEffect, useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' +import { UserComponent } from '../../components/username' import { MetadataController, NostrController } from '../../controllers' -import { appPrivateRoutes, getProfileRoute } from '../../routes' +import { appPrivateRoutes } from '../../routes' +import { State } from '../../store/rootReducer' import { ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, generateEncryptionKey, getHash, - getRoboHashPicture, hexToNpub, pubToHex, queryNip05, @@ -39,10 +43,6 @@ import { uploadToFileStorage } from '../../utils' import styles from './style.module.scss' -import { toast } from 'react-toastify' -import JSZip from 'jszip' -import { useSelector } from 'react-redux' -import { State } from '../../store/rootReducer' export const CreatePage = () => { const navigate = useNavigate() @@ -489,15 +489,6 @@ const DisplayUser = ({ }) }, [users]) - /** - * Use robohash if any of the users images fail to load - * @param event img tag onError event - * @param pubkey of the user - */ - const imageLoadError = (event: any, pubkey: string) => { - event.target.src = getRoboHashPicture(pubkey) - } - return ( @@ -511,33 +502,18 @@ const DisplayUser = ({ {users.map((user, index) => { const userMeta = metadata[user.pubkey] - const npub = hexToNpub(user.pubkey) - const roboUrl = `https://robohash.org/${npub}.png?set=set3` return ( - - { - imageLoadError(event, user.pubkey) - }} - src={userMeta?.picture || roboUrl} - alt="Profile Image" - className="profile-image" - style={{ - borderWidth: '3px', - borderStyle: 'solid', - borderColor: `#${user.pubkey.substring(0, 6)}` - }} - /> - - - {userMeta?.display_name || - userMeta?.name || - shorten(npub)} - - - +
        - User - Role + User + Role Action - {users.map((user, index) => { - const userMeta = metadata[user.pubkey] - return ( - - - - - - - - - - handleRemoveUser(user.pubkey)}> - - - - - - ) - })} + + {users + .filter((user) => user.role === UserRole.signer) + .map((user, index) => ( + + ))} + + {users + .filter((user) => user.role === UserRole.viewer) + .map((user, index) => { + const userMeta = metadata[user.pubkey] + return ( + + + + + + + + + + handleRemoveUser(user.pubkey)}> + + + + + + ) + })}
        ) } + +interface DragItem { + index: number + id: string + type: string +} + +type SignerRowProps = { + userMeta: ProfileMetadata + user: User + index: number + moveSigner: (dragIndex: number, hoverIndex: number) => void + handleUserRoleChange: (role: UserRole, pubkey: string) => void + handleRemoveUser: (pubkey: string) => void +} + +const SignerRow = ({ + userMeta, + user, + index, + moveSigner, + handleUserRoleChange, + handleRemoveUser +}: SignerRowProps) => { + const ref = useRef(null) + + const [{ handlerId }, drop] = useDrop< + DragItem, + void, + { handlerId: Identifier | null } + >({ + accept: 'row', + collect(monitor) { + return { + handlerId: monitor.getHandlerId() + } + }, + hover(item: DragItem, monitor) { + if (!ref.current) { + return + } + const dragIndex = item.index + const hoverIndex = index + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return + } + + // Determine rectangle on screen + const hoverBoundingRect = ref.current?.getBoundingClientRect() + + // Get vertical middle + const hoverMiddleY = + (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + + // Determine mouse position + const clientOffset = monitor.getClientOffset() + + // Get pixels to the top + const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top + + // Only perform the move when the mouse has crossed half of the items height + // When dragging downwards, only move when the cursor is below 50% + // When dragging upwards, only move when the cursor is above 50% + + // Dragging downwards + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return + } + + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return + } + + // Time to actually perform the action + moveSigner(dragIndex, hoverIndex) + + // Note: we're mutating the monitor item here! + // Generally it's better to avoid mutations, + // but it's good here for the sake of performance + // to avoid expensive index searches. + item.index = hoverIndex + } + }) + + const [{ isDragging }, drag] = useDrag({ + type: 'row', + item: () => { + return { id: user.pubkey, index } + }, + collect: (monitor: any) => ({ + isDragging: monitor.isDragging() + }) + }) + + const opacity = isDragging ? 0 : 1 + drag(drop(ref)) + + return ( + + + + + + + + + + + handleRemoveUser(user.pubkey)}> + + + + + + ) +} diff --git a/src/pages/create/style.module.scss b/src/pages/create/style.module.scss index 90804ee..2cd2af8 100644 --- a/src/pages/create/style.module.scss +++ b/src/pages/create/style.module.scss @@ -17,8 +17,13 @@ border-bottom: 0.5px solid; } +.tableHeaderCell { + border-right: 1px solid rgba(224, 224, 224, 1); +} + .tableCell { border-right: 1px solid rgba(224, 224, 224, 1); + height: 56px; .user { display: flex; -- 2.34.1 From d10e243ba3fcf99aa536c358e32fe57198538449 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Mon, 20 May 2024 12:19:53 +0500 Subject: [PATCH 46/48] chore fix type --- src/pages/create/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 6b7a2a3..619a9f0 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -168,7 +168,13 @@ export const CreatePage = () => { setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey)) } - const moveSinger = (dragIndex: number, hoverIndex: number) => { + /** + * changes the position of signer in the signers list + * + * @param dragIndex represents the current position of user + * @param hoverIndex represents the target position of user + */ + const moveSigner = (dragIndex: number, hoverIndex: number) => { setUsers((prevUsers) => { const updatedUsers = [...prevUsers] const [draggedUser] = updatedUsers.splice(dragIndex, 1) @@ -448,7 +454,7 @@ export const CreatePage = () => { users={users} handleUserRoleChange={handleUserRoleChange} handleRemoveUser={handleRemoveUser} - moveSigner={moveSinger} + moveSigner={moveSigner} />