Profile pictures incosistencies #58
@ -25,6 +25,7 @@ import {
|
||||
shorten
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { setUserRobotImage } from '../../store/userRobotImage/action'
|
||||
|
||||
const metadataController = new MetadataController()
|
||||
|
||||
@ -39,6 +40,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) {
|
||||
@ -47,17 +49,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<HTMLElement>) => {
|
||||
setAnchorElUser(event.currentTarget)
|
||||
@ -85,8 +87,8 @@ export const AppBar = () => {
|
||||
nsecBunkerPubkey: undefined
|
||||
})
|
||||
)
|
||||
|
||||
dispatch(setMetadataEvent(metadataController.getEmptyMetadataEvent()))
|
||||
dispatch(setUserRobotImage(null))
|
||||
m marked this conversation as resolved
|
||||
|
||||
// clear authToken saved in local storage
|
||||
clearAuthToken()
|
||||
|
@ -34,34 +34,10 @@ export class AuthController {
|
||||
async authenticateAndFindMetadata(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))
|
||||
|
@ -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)
|
||||
|
@ -1,13 +1,14 @@
|
||||
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,
|
||||
getRoboHashPicture,
|
||||
loadState,
|
||||
saveNsecBunkerDelegatedKey
|
||||
} from '../utils'
|
||||
@ -15,12 +16,15 @@ 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 = () => {
|
||||
@ -70,6 +74,21 @@ 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 <LoadingSpinner desc="Loading App" />
|
||||
|
||||
return (
|
||||
|
@ -14,7 +14,8 @@ store.subscribe(
|
||||
_.throttle(() => {
|
||||
saveState({
|
||||
auth: store.getState().auth,
|
||||
metadata: store.getState().metadata
|
||||
metadata: store.getState().metadata,
|
||||
userRobotImage: store.getState().userRobotImage
|
||||
})
|
||||
}, 1000)
|
||||
)
|
||||
|
@ -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 = () => {
|
||||
<div className={styles.loginPage}>
|
||||
<Typography variant="h4">Welcome to Sigit</Typography>
|
||||
<TextField
|
||||
onKeyDown={handleInputKeyDown}
|
||||
label="nip05 login / nip46 bunker string / nsec"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||
import {
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
InputProps,
|
||||
List,
|
||||
@ -12,7 +11,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 +25,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()
|
||||
@ -42,16 +42,18 @@ export const ProfilePage = () => {
|
||||
useState<NostrJoiningBlock | null>(null)
|
||||
const [profileMetadata, setProfileMetadata] = useState<ProfileMetadata>()
|
||||
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)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [loadingSpinnerDesc] = useState('Fetching metadata')
|
||||
|
||||
const robotSet = useRef(1)
|
||||
|
||||
useEffect(() => {
|
||||
if (npub) {
|
||||
try {
|
||||
@ -210,22 +212,21 @@ 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
|
||||
|
||||
const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3`
|
||||
const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current)
|
||||
|
||||
setProfileMetadata((prev) => ({
|
||||
...prev,
|
||||
picture: ''
|
||||
picture: robotAvatarLink
|
||||
}))
|
||||
|
||||
setTimeout(() => {
|
||||
setProfileMetadata((prev) => ({
|
||||
...prev,
|
||||
picture: robotAvatarLink
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -233,21 +234,31 @@ export const ProfilePage = () => {
|
||||
* @returns robohash generate button, loading spinner or no button
|
||||
*/
|
||||
const robohashButton = () => {
|
||||
if (profileMetadata?.picture?.includes('robohash')) return null
|
||||
|
||||
return (
|
||||
<Tooltip title="Generate a robohash avatar">
|
||||
{avatarLoading ? (
|
||||
<CircularProgress style={{ padding: 8 }} size={22} />
|
||||
) : (
|
||||
<IconButton onClick={generateRobotAvatar}>
|
||||
<SmartToy />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={generateRobotAvatar}>
|
||||
<SmartToy />
|
||||
</IconButton>
|
||||
</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 (
|
||||
<>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
@ -280,11 +291,11 @@ export const ProfilePage = () => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onLoad={() => {
|
||||
setAvatarLoading(false)
|
||||
onError={(event: any) => {
|
||||
event.target.src = getRoboHashPicture(npub!)
|
||||
}}
|
||||
className={styles.img}
|
||||
src={profileMetadata.picture}
|
||||
src={getProfileImage(profileMetadata)}
|
||||
alt="Profile Image"
|
||||
/>
|
||||
|
||||
@ -305,7 +316,7 @@ export const ProfilePage = () => {
|
||||
</ListItem>
|
||||
|
||||
{editItem('picture', 'Picture URL', undefined, undefined, {
|
||||
endAdornment: robohashButton()
|
||||
endAdornment: isUsersOwnProfile ? robohashButton() : undefined
|
||||
})}
|
||||
|
||||
{editItem('name', 'Username')}
|
||||
|
@ -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_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
|
||||
|
@ -3,13 +3,16 @@ import { combineReducers } from 'redux'
|
||||
import authReducer from './auth/reducer'
|
||||
import { AuthState } from './auth/types'
|
||||
import metadataReducer from './metadata/reducer'
|
||||
import userRobotImageReducer from './userRobotImage/reducer'
|
||||
|
||||
export interface State {
|
||||
auth: AuthState
|
||||
metadata?: Event
|
||||
userRobotImage?: string
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
auth: authReducer,
|
||||
metadata: metadataReducer
|
||||
metadata: metadataReducer,
|
||||
userRobotImage: userRobotImageReducer
|
||||
})
|
||||
|
9
src/store/userRobotImage/action.ts
Normal file
9
src/store/userRobotImage/action.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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 | undefined => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.SET_USER_ROBOT_IMAGE:
|
||||
return action.payload
|
||||
|
||||
case ActionTypes.RESTORE_STATE:
|
||||
return action.payload.userRobotImage
|
||||
m marked this conversation as resolved
Outdated
y
commented
`|| null` is not needed in such case
|
||||
|
||||
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
|
@ -143,7 +143,11 @@ 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 => {
|
||||
if (!pubkey) return ''
|
||||
const npub = hexToNpub(pubkey)
|
||||
return `https://robohash.org/${npub}.png?set=set3`
|
||||
return `https://robohash.org/${npub}.png?set=set${set}`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user
the app will clear local storage on logout anyway
This is a redux bit, won't get cleared automatically.