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}` }