Add blurhash support for user profile images in ActivityIndicator component
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Integrated the blurhash library to decode and display blurred placeholders for user profile images. - Updated UserProfileImage component to accept a blurHash prop and render the decoded image when available. - Enhanced UserActivityAvatar to retrieve and pass the blurHash from user data, improving the visual experience during image loading.
This commit is contained in:
parent
8d0f92af45
commit
28dfb645ef
@ -42,6 +42,7 @@
|
|||||||
"antd": "^5.27.1",
|
"antd": "^5.27.1",
|
||||||
"antd-style": "^3.7.1",
|
"antd-style": "^3.7.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
"country-list": "^2.4.1",
|
"country-list": "^2.4.1",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -107,6 +107,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.11.0
|
specifier: ^1.11.0
|
||||||
version: 1.13.4
|
version: 1.13.4
|
||||||
|
blurhash:
|
||||||
|
specifier: ^2.0.5
|
||||||
|
version: 2.0.5
|
||||||
country-list:
|
country-list:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
@ -2358,6 +2361,9 @@ packages:
|
|||||||
bluebird@3.7.2:
|
bluebird@3.7.2:
|
||||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||||
|
|
||||||
|
blurhash@2.0.5:
|
||||||
|
resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==}
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -8644,6 +8650,8 @@ snapshots:
|
|||||||
|
|
||||||
bluebird@3.7.2: {}
|
bluebird@3.7.2: {}
|
||||||
|
|
||||||
|
blurhash@2.0.5: {}
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useContext, useRef, memo } from 'react'
|
import { useState, useEffect, useContext, useRef, memo } from 'react'
|
||||||
import { Flex, Card, Popover, Divider } from 'antd'
|
import { Flex, Card, Popover, Divider } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
import { decode } from 'blurhash'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import EditIcon from '../../Icons/EditIcon'
|
import EditIcon from '../../Icons/EditIcon'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
@ -19,6 +20,14 @@ const getProfileImageId = (user) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProfileImageBlurHash = (user) => {
|
||||||
|
const profileImage = user?.profileImage
|
||||||
|
if (typeof profileImage === 'object' && profileImage !== null) {
|
||||||
|
return profileImage.metaData?.blurHash || null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const getDisplayName = (user) =>
|
const getDisplayName = (user) =>
|
||||||
user?.name ||
|
user?.name ||
|
||||||
[user?.firstName, user?.lastName].filter(Boolean).join(' ') ||
|
[user?.firstName, user?.lastName].filter(Boolean).join(' ') ||
|
||||||
@ -37,16 +46,39 @@ const avatarCardStyle = { borderRadius: '12px', width: 32, height: 32 }
|
|||||||
|
|
||||||
const UserProfileImage = memo(function UserProfileImage({
|
const UserProfileImage = memo(function UserProfileImage({
|
||||||
profileImageId,
|
profileImageId,
|
||||||
displayName
|
displayName,
|
||||||
|
blurHash
|
||||||
}) {
|
}) {
|
||||||
const { fetchFileThumbnail } = useContext(ApiServerContext)
|
const { fetchFileThumbnail } = useContext(ApiServerContext)
|
||||||
const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
|
const fetchFileThumbnailRef = useRef(fetchFileThumbnail)
|
||||||
fetchFileThumbnailRef.current = fetchFileThumbnail
|
fetchFileThumbnailRef.current = fetchFileThumbnail
|
||||||
|
|
||||||
const [profileImageUrl, setProfileImageUrl] = useState(null)
|
const [profileImageUrl, setProfileImageUrl] = useState(null)
|
||||||
|
const [blurHashUrl, setBlurHashUrl] = useState(null)
|
||||||
const profileImageUrlRef = useRef(null)
|
const profileImageUrlRef = useRef(null)
|
||||||
const loadedImageIdRef = useRef(null)
|
const loadedImageIdRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!blurHash) {
|
||||||
|
setBlurHashUrl(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pixels = decode(blurHash, 32, 32)
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 32
|
||||||
|
canvas.height = 32
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const imageData = ctx.createImageData(32, 32)
|
||||||
|
imageData.data.set(pixels)
|
||||||
|
ctx.putImageData(imageData, 0, 0)
|
||||||
|
setBlurHashUrl(canvas.toDataURL())
|
||||||
|
} catch {
|
||||||
|
setBlurHashUrl(null)
|
||||||
|
}
|
||||||
|
}, [blurHash])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!profileImageId) {
|
if (!profileImageId) {
|
||||||
if (profileImageUrlRef.current) {
|
if (profileImageUrlRef.current) {
|
||||||
@ -111,6 +143,17 @@ const UserProfileImage = memo(function UserProfileImage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileImageId && blurHashUrl) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={blurHashUrl}
|
||||||
|
alt=''
|
||||||
|
aria-hidden
|
||||||
|
style={avatarImageStyle}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
style={avatarCardStyle}
|
style={avatarCardStyle}
|
||||||
@ -130,12 +173,14 @@ const UserProfileImage = memo(function UserProfileImage({
|
|||||||
|
|
||||||
UserProfileImage.propTypes = {
|
UserProfileImage.propTypes = {
|
||||||
profileImageId: PropTypes.string,
|
profileImageId: PropTypes.string,
|
||||||
displayName: PropTypes.string.isRequired
|
displayName: PropTypes.string.isRequired,
|
||||||
|
blurHash: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserActivityAvatar = memo(
|
const UserActivityAvatar = memo(
|
||||||
function UserActivityAvatar({ user, mode }) {
|
function UserActivityAvatar({ user, mode }) {
|
||||||
const profileImageId = getProfileImageId(user)
|
const profileImageId = getProfileImageId(user)
|
||||||
|
const blurHash = getProfileImageBlurHash(user)
|
||||||
const displayName = getDisplayName(user)
|
const displayName = getDisplayName(user)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -173,6 +218,7 @@ const UserActivityAvatar = memo(
|
|||||||
<UserProfileImage
|
<UserProfileImage
|
||||||
profileImageId={profileImageId}
|
profileImageId={profileImageId}
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
|
blurHash={blurHash}
|
||||||
/>
|
/>
|
||||||
{mode === 'editing' && (
|
{mode === 'editing' && (
|
||||||
<div
|
<div
|
||||||
@ -201,7 +247,9 @@ const UserActivityAvatar = memo(
|
|||||||
return (
|
return (
|
||||||
prevProps.mode === nextProps.mode &&
|
prevProps.mode === nextProps.mode &&
|
||||||
getUserId(prevProps.user) === getUserId(nextProps.user) &&
|
getUserId(prevProps.user) === getUserId(nextProps.user) &&
|
||||||
getProfileImageId(prevProps.user) === getProfileImageId(nextProps.user)
|
getProfileImageId(prevProps.user) === getProfileImageId(nextProps.user) &&
|
||||||
|
getProfileImageBlurHash(prevProps.user) ===
|
||||||
|
getProfileImageBlurHash(nextProps.user)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user