issue-38 #62
@ -29,6 +29,7 @@ import {
|
|||||||
shorten
|
shorten
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||||
|
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ export const AppBar = () => {
|
|||||||
|
|
||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
const metadataState = useSelector((state: State) => state.metadata)
|
const metadataState = useSelector((state: State) => state.metadata)
|
||||||
|
const userRobotImage = useSelector((state: State) => state.userRobotImage)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (metadataState) {
|
if (metadataState) {
|
||||||
@ -51,17 +53,17 @@ export const AppBar = () => {
|
|||||||
metadataState.content
|
metadataState.content
|
||||||
)
|
)
|
||||||
|
|
||||||
if (picture) {
|
if (picture || userRobotImage) {
|
||||||
setUserAvatar(picture)
|
setUserAvatar(picture || userRobotImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsername(shorten(display_name || name || '', 7))
|
setUsername(shorten(display_name || name || '', 7))
|
||||||
} else {
|
} else {
|
||||||
setUserAvatar('')
|
setUserAvatar(userRobotImage || '')
|
||||||
setUsername('')
|
setUsername('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [metadataState])
|
}, [metadataState, userRobotImage])
|
||||||
|
|
||||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorElUser(event.currentTarget)
|
setAnchorElUser(event.currentTarget)
|
||||||
@ -89,8 +91,8 @@ export const AppBar = () => {
|
|||||||
nsecBunkerPubkey: undefined
|
nsecBunkerPubkey: undefined
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
|
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
|
||||||
|
dispatch(setUserRobotImage(null))
|
||||||
|
|
||||||
// clear authToken saved in local storage
|
// clear authToken saved in local storage
|
||||||
clearAuthToken()
|
clearAuthToken()
|
||||||
|
@ -39,34 +39,10 @@ export class AuthController {
|
|||||||
async authAndGetMetadataAndRelaysMap(pubkey: string) {
|
async authAndGetMetadataAndRelaysMap(pubkey: string) {
|
||||||
const emptyMetadata = this.metadataController.getEmptyMetadataEvent()
|
const emptyMetadata = this.metadataController.getEmptyMetadataEvent()
|
||||||
|
|
||||||
emptyMetadata.content = JSON.stringify({
|
|
||||||
picture: getRoboHashPicture(pubkey)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.metadataController
|
this.metadataController
|
||||||
.findMetadata(pubkey)
|
.findMetadata(pubkey)
|
||||||
.then((event) => {
|
.then((event) => {
|
||||||
if (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))
|
store.dispatch(setMetadataEvent(event))
|
||||||
} else {
|
} else {
|
||||||
store.dispatch(setMetadataEvent(emptyMetadata))
|
store.dispatch(setMetadataEvent(emptyMetadata))
|
||||||
|
@ -142,6 +142,7 @@ export class MetadataController {
|
|||||||
|
|
||||||
public extractProfileMetadataContent = (event: VerifiedEvent) => {
|
public extractProfileMetadataContent = (event: VerifiedEvent) => {
|
||||||
try {
|
try {
|
||||||
|
if (!event.content) return {}
|
||||||
return JSON.parse(event.content) as ProfileMetadata
|
return JSON.parse(event.content) as ProfileMetadata
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error in parsing metadata event content :>> ', error)
|
console.log('error in parsing metadata event content :>> ', error)
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import Container from '@mui/material/Container'
|
import Container from '@mui/material/Container'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { AppBar } from '../components/AppBar/AppBar'
|
import { AppBar } from '../components/AppBar/AppBar'
|
||||||
import { restoreState, setAuthState, setMetadataEvent } from '../store/actions'
|
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 { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
import { Dispatch } from '../store/store'
|
import { Dispatch } from '../store/store'
|
||||||
import { MetadataController, NostrController } from '../controllers'
|
import { MetadataController, NostrController } from '../controllers'
|
||||||
import { LoginMethods } from '../store/auth/types'
|
import { LoginMethods } from '../store/auth/types'
|
||||||
|
import { setUserRobotImage } from '../store/userRobotImage/action'
|
||||||
|
import { State } from '../store/rootReducer'
|
||||||
|
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
const dispatch: Dispatch = useDispatch()
|
const dispatch: Dispatch = useDispatch()
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const authState = useSelector((state: State) => state.auth)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
@ -67,6 +70,23 @@ export const MainLayout = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, [dispatch])
|
}, [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 <LoadingSpinner desc="Loading App" />
|
if (isLoading) return <LoadingSpinner desc="Loading App" />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,7 +15,8 @@ store.subscribe(
|
|||||||
saveState({
|
saveState({
|
||||||
auth: store.getState().auth,
|
auth: store.getState().auth,
|
||||||
metadata: store.getState().metadata,
|
metadata: store.getState().metadata,
|
||||||
relays: store.getState().relays
|
relays: store.getState().relays,
|
||||||
|
userRobotImage: store.getState().userRobotImage
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
)
|
)
|
||||||
|
@ -43,6 +43,16 @@ export const Login = () => {
|
|||||||
}, 500)
|
}, 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 () => {
|
const loginWithExtension = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
setLoadingSpinnerDesc('Capturing pubkey from nostr extension')
|
||||||
@ -303,6 +313,7 @@ export const Login = () => {
|
|||||||
<div className={styles.loginPage}>
|
<div className={styles.loginPage}>
|
||||||
<Typography variant="h4">Welcome to Sigit</Typography>
|
<Typography variant="h4">Welcome to Sigit</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
label="nip05 login / nip46 bunker string / nsec"
|
label="nip05 login / nip46 bunker string / nsec"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import {
|
import {
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
InputProps,
|
InputProps,
|
||||||
List,
|
List,
|
||||||
@ -43,10 +42,10 @@ export const ProfilePage = () => {
|
|||||||
useState<NostrJoiningBlock | null>(null)
|
useState<NostrJoiningBlock | null>(null)
|
||||||
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
||||||
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
|
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
|
||||||
const [avatarLoading, setAvatarLoading] = useState(false)
|
|
||||||
const metadataState = useSelector((state: State) => state.metadata)
|
const metadataState = useSelector((state: State) => state.metadata)
|
||||||
const keys = useSelector((state: State) => state.auth?.keyPair)
|
const keys = useSelector((state: State) => state.auth?.keyPair)
|
||||||
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth)
|
||||||
|
const userRobotImage = useSelector((state: State) => state.userRobotImage)
|
||||||
|
|
||||||
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
|
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
|
||||||
|
|
||||||
@ -213,9 +212,12 @@ export const ProfilePage = () => {
|
|||||||
setSavingProfileMetadata(false)
|
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 = () => {
|
const generateRobotAvatar = () => {
|
||||||
setAvatarLoading(true)
|
|
||||||
|
|
||||||
robotSet.current++
|
robotSet.current++
|
||||||
if (robotSet.current > 5) robotSet.current = 1
|
if (robotSet.current > 5) robotSet.current = 1
|
||||||
|
|
||||||
@ -223,15 +225,8 @@ export const ProfilePage = () => {
|
|||||||
|
|
||||||
setProfileMetadata((prev) => ({
|
setProfileMetadata((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
picture: ''
|
picture: robotAvatarLink
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setProfileMetadata((prev) => ({
|
|
||||||
...prev,
|
|
||||||
picture: robotAvatarLink
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -241,17 +236,29 @@ export const ProfilePage = () => {
|
|||||||
const robohashButton = () => {
|
const robohashButton = () => {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Generate a robohash avatar">
|
<Tooltip title="Generate a robohash avatar">
|
||||||
{avatarLoading ? (
|
<IconButton onClick={generateRobotAvatar}>
|
||||||
<CircularProgress style={{ padding: 8 }} size={22} />
|
<SmartToy />
|
||||||
) : (
|
</IconButton>
|
||||||
<IconButton onClick={generateRobotAvatar}>
|
|
||||||
<SmartToy />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
@ -285,13 +292,10 @@ export const ProfilePage = () => {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onError={(event: any) => {
|
onError={(event: any) => {
|
||||||
event.target.src = npub ? getRoboHashPicture(npub) : ''
|
event.target.src = getRoboHashPicture(npub!)
|
||||||
}}
|
|
||||||
onLoad={() => {
|
|
||||||
setAvatarLoading(false)
|
|
||||||
}}
|
}}
|
||||||
className={styles.img}
|
className={styles.img}
|
||||||
src={profileMetadata.picture}
|
src={getProfileImage(profileMetadata)}
|
||||||
alt="Profile Image"
|
alt="Profile Image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -9,3 +9,5 @@ export const UPDATE_NSECBUNKER_RELAYS = 'UPDATE_NSECBUNKER_RELAYS'
|
|||||||
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
export const SET_METADATA_EVENT = 'SET_METADATA_EVENT'
|
||||||
|
|
||||||
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
export const SET_RELAY_MAP = 'SET_RELAY_MAP'
|
||||||
|
|
||||||
|
export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
||||||
|
@ -5,15 +5,18 @@ import { AuthState } from './auth/types'
|
|||||||
import metadataReducer from './metadata/reducer'
|
import metadataReducer from './metadata/reducer'
|
||||||
import { RelaysState } from './relays/types'
|
import { RelaysState } from './relays/types'
|
||||||
import relaysReducer from './relays/reducer'
|
import relaysReducer from './relays/reducer'
|
||||||
|
import userRobotImageReducer from './userRobotImage/reducer'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
auth: AuthState
|
auth: AuthState
|
||||||
relays: RelaysState
|
relays: RelaysState
|
||||||
metadata?: Event
|
metadata?: Event
|
||||||
|
userRobotImage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
metadata: metadataReducer,
|
metadata: metadataReducer,
|
||||||
relays: relaysReducer
|
relays: relaysReducer,
|
||||||
|
userRobotImage: userRobotImageReducer
|
||||||
})
|
})
|
||||||
|
7
src/store/userRobotImage/action.ts
Normal file
7
src/store/userRobotImage/action.ts
Normal file
@ -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
|
||||||
|
})
|
22
src/store/userRobotImage/reducer.ts
Normal file
22
src/store/userRobotImage/reducer.ts
Normal file
@ -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
|
9
src/store/userRobotImage/types.ts
Normal file
9
src/store/userRobotImage/types.ts
Normal file
@ -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
|
@ -145,7 +145,8 @@ export const base64DecodeAuthToken = (authToken: string): SignedEvent => {
|
|||||||
* @param pubkey in hex or npub format
|
* @param pubkey in hex or npub format
|
||||||
* @returns robohash.org url for the avatar
|
* @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)
|
const npub = hexToNpub(pubkey)
|
||||||
return `https://robohash.org/${npub}.png?set=set${set}`
|
return `https://robohash.org/${npub}.png?set=set${set}`
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user