From 28dfb645ef816d2f701beed91f9380c21ad35b20 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 26 Jul 2026 21:28:43 +0100 Subject: [PATCH] Add blurhash support for user profile images in ActivityIndicator component - 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. --- package.json | 1 + pnpm-lock.yaml | 8 +++ .../Dashboard/common/ActivityIndicator.jsx | 54 +++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index df428f0..ad70bbe 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "antd": "^5.27.1", "antd-style": "^3.7.1", "axios": "^1.11.0", + "blurhash": "^2.0.5", "country-list": "^2.4.1", "cross-env": "^10.0.0", "dayjs": "^1.11.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e141c01..14d4303 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: axios: specifier: ^1.11.0 version: 1.13.4 + blurhash: + specifier: ^2.0.5 + version: 2.0.5 country-list: specifier: ^2.4.1 version: 2.4.1 @@ -2358,6 +2361,9 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -8644,6 +8650,8 @@ snapshots: bluebird@3.7.2: {} + blurhash@2.0.5: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 diff --git a/src/components/Dashboard/common/ActivityIndicator.jsx b/src/components/Dashboard/common/ActivityIndicator.jsx index 7f8067b..fcd1453 100644 --- a/src/components/Dashboard/common/ActivityIndicator.jsx +++ b/src/components/Dashboard/common/ActivityIndicator.jsx @@ -1,6 +1,7 @@ import { useState, useEffect, useContext, useRef, memo } from 'react' import { Flex, Card, Popover, Divider } from 'antd' import { UserOutlined } from '@ant-design/icons' +import { decode } from 'blurhash' import PropTypes from 'prop-types' import EditIcon from '../../Icons/EditIcon' 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) => user?.name || [user?.firstName, user?.lastName].filter(Boolean).join(' ') || @@ -37,16 +46,39 @@ const avatarCardStyle = { borderRadius: '12px', width: 32, height: 32 } const UserProfileImage = memo(function UserProfileImage({ profileImageId, - displayName + displayName, + blurHash }) { const { fetchFileThumbnail } = useContext(ApiServerContext) const fetchFileThumbnailRef = useRef(fetchFileThumbnail) fetchFileThumbnailRef.current = fetchFileThumbnail const [profileImageUrl, setProfileImageUrl] = useState(null) + const [blurHashUrl, setBlurHashUrl] = useState(null) const profileImageUrlRef = 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(() => { if (!profileImageId) { if (profileImageUrlRef.current) { @@ -111,6 +143,17 @@ const UserProfileImage = memo(function UserProfileImage({ ) } + if (profileImageId && blurHashUrl) { + return ( + + ) + } + return ( {mode === 'editing' && (