@@ -30,6 +30,8 @@ import SlideInRight from "./transitions/SlideInRight";
3030import { useColorMode } from "../contexts/ColorModeProvider" ;
3131import { addMessagingContact } from "../utils/contacts" ;
3232import {
33+ canUpdateGroupProfile ,
34+ GROUP_PROFILE_PAYLOAD_TYPE ,
3335 avatarDataUrl ,
3436 formatTime ,
3537 getChatMessageText ,
@@ -78,6 +80,8 @@ const ChatWindow: React.FC = () => {
7880 profileList,
7981 senderProfileList,
8082 groupList,
83+ setGroupList,
84+ resolveAvatar,
8185 resolveSenderProfile,
8286 } = useVariablesContext ( ) ;
8387 const {
@@ -93,10 +97,12 @@ const ChatWindow: React.FC = () => {
9397 const [ imageViewerSrc , setImageViewerSrc ] = useState < string > ( "" ) ;
9498 const [ imageViewerOpen , setImageViewerOpen ] = useState < boolean > ( false ) ;
9599 const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
100+ const groupAvatarInputRef = useRef < HTMLInputElement | null > ( null ) ;
96101 const [ imageAttachments , setImageAttachments ] = useState < Record < string , { name : string ; url : string ; mime : string } [ ] > > ( { } ) ;
97102 const [ currentGroupMembers , setCurrentGroupMembers ] = useState < string [ ] > ( [ ] ) ;
98103 const [ groupDetailsOpen , setGroupDetailsOpen ] = useState < boolean > ( false ) ;
99104 const [ selectedMember , setSelectedMember ] = useState < GroupMemberDisplay | null > ( null ) ;
105+ const [ updatingGroupAvatar , setUpdatingGroupAvatar ] = useState < boolean > ( false ) ;
100106
101107 const { setError, setSuccess } = useSnackbar ( ) ;
102108 const { colorMode } = useColorMode ( ) ;
@@ -249,6 +255,10 @@ const ChatWindow: React.FC = () => {
249255 : false ;
250256 const selectedMemberCanAdd = ! ! selectedMember && ! selectedMember . isCurrentUser && ! selectedMemberAlreadyKnown ;
251257 const memberCountLabel = `${ currentGroupMembers . length } ${ currentGroupMembers . length === 1 ? "member" : "members" } ` ;
258+ const canUpdateActiveGroupAvatar = isGroup && canUpdateGroupProfile (
259+ currentDID ,
260+ { members : currentGroupMembers . length ? currentGroupMembers : ( groupEntry ?. members ?? [ ] ) }
261+ ) ;
252262
253263 const onRemove = ( ) => {
254264 if ( ! activePeer ) {
@@ -467,6 +477,98 @@ const ChatWindow: React.FC = () => {
467477 }
468478 }
469479
480+ const openGroupAvatarPicker = ( ) => {
481+ if ( ! isGroup || updatingGroupAvatar || ! groupAvatarInputRef . current ) {
482+ return ;
483+ }
484+
485+ groupAvatarInputRef . current . click ( ) ;
486+ } ;
487+
488+ const uploadGroupAvatar = async ( event : React . ChangeEvent < HTMLInputElement > ) => {
489+ const fileInput = event . target ;
490+ const file = fileInput . files ?. [ 0 ] ;
491+
492+ if ( ! file || ! keymaster || ! activePeer || ! isGroup ) {
493+ fileInput . value = "" ;
494+ return ;
495+ }
496+
497+ if ( ! isImageName ( file . name ) ) {
498+ setError ( "Please select an image file" ) ;
499+ fileInput . value = "" ;
500+ return ;
501+ }
502+
503+ try {
504+ setUpdatingGroupAvatar ( true ) ;
505+
506+ const group = await keymaster . getGroup ( activePeer ) ;
507+ if ( ! group ?. members ) {
508+ setError ( GROUP_NOT_FOUND ) ;
509+ return ;
510+ }
511+
512+ if ( ! canUpdateGroupProfile ( currentDID , group ) ) {
513+ setError ( "Only group members can update the group avatar" ) ;
514+ return ;
515+ }
516+
517+ const arrayBuffer = await file . arrayBuffer ( ) ;
518+ const assetDid = await keymaster . createImage ( Buffer . from ( arrayBuffer ) ) ;
519+ const avatar = await resolveAvatar ( assetDid ) ;
520+ if ( ! avatar ) {
521+ throw new Error ( "Group avatar image could not be loaded" ) ;
522+ }
523+
524+ const updatedAt = new Date ( ) . toISOString ( ) ;
525+ const recipients = Array . from ( new Set ( group . members ) ) ;
526+ const payload : ChatPayload = {
527+ type : GROUP_PROFILE_PAYLOAD_TYPE ,
528+ version : 1 ,
529+ groupId : activePeer ,
530+ groupAvatar : assetDid ,
531+ updatedAt,
532+ } ;
533+
534+ const dmail = {
535+ to : recipients ,
536+ cc : [ ] ,
537+ subject : CHAT_SUBJECT ,
538+ body : stringifyChatPayload ( payload ) ,
539+ } ;
540+
541+ const did = await keymaster . createDmail ( dmail , { registry } ) ;
542+ await keymaster . sendDmail ( did ) ;
543+
544+ setGroupList ( prev => {
545+ const existing = prev [ activePeer ] ?? {
546+ name : peerDisplayName ,
547+ members : group . members ,
548+ } ;
549+
550+ return {
551+ ...prev ,
552+ [ activePeer ] : {
553+ ...existing ,
554+ members : group . members ,
555+ avatar,
556+ avatarDid : assetDid ,
557+ avatarUpdatedAt : updatedAt ,
558+ } ,
559+ } ;
560+ } ) ;
561+
562+ await refreshInbox ( ) ;
563+ setSuccess ( "Group avatar updated" ) ;
564+ } catch ( error : any ) {
565+ setError ( error ) ;
566+ } finally {
567+ setUpdatingGroupAvatar ( false ) ;
568+ fileInput . value = "" ;
569+ }
570+ } ;
571+
470572 const uploadImageAttachment = async ( event : React . ChangeEvent < HTMLInputElement > ) => {
471573 if ( ! keymaster || ! activePeer ) {
472574 return ;
@@ -779,7 +881,11 @@ const ChatWindow: React.FC = () => {
779881 } , [ conversation , keymaster ] ) ;
780882 const profile = uiPeer ? profileList [ uiPeer ] : undefined ;
781883 const customAvatarUrl = profile ?. avatar ;
782- const peerAvatar = uiPeer ? ( customAvatarUrl ?? avatarDataUrl ( peerDid || peerDisplayName ) ) : "" ;
884+ const peerAvatar = uiPeer
885+ ? ( isGroup
886+ ? ( groupEntry ?. avatar ?? avatarDataUrl ( peerDid || peerDisplayName ) )
887+ : ( customAvatarUrl ?? avatarDataUrl ( peerDid || peerDisplayName ) ) )
888+ : "" ;
783889
784890 return (
785891 < >
@@ -812,6 +918,9 @@ const ChatWindow: React.FC = () => {
812918 groupName = { peerDisplayName }
813919 members = { groupMemberDisplays }
814920 onMemberSelect = { ( member ) => setSelectedMember ( member ) }
921+ onGroupAvatarClick = { openGroupAvatarPicker }
922+ groupAvatarUpdating = { updatingGroupAvatar }
923+ canUpdateGroupAvatar = { canUpdateActiveGroupAvatar }
815924 />
816925
817926 { selectedMember && (
@@ -827,12 +936,25 @@ const ChatWindow: React.FC = () => {
827936 />
828937 ) }
829938
939+ < input
940+ ref = { groupAvatarInputRef }
941+ type = "file"
942+ accept = "image/*"
943+ hidden
944+ onChange = { uploadGroupAvatar }
945+ />
946+
830947 < SlideInRight isOpen = { ! ! activePeer } bg = { colorMode === "dark" ? "gray.900" : "white" } bottomOffset = { 0 } zIndex = { 2200 } position = "fixed" >
831948 { uiPeer && (
832949 < ChatContainer style = { { height : "100%" } } >
833950 < ConversationHeader >
834951 < ConversationHeader . Back onClick = { onBack } />
835- < Avatar src = { peerAvatar } name = { uiPeer } />
952+ < Avatar
953+ src = { peerAvatar }
954+ name = { uiPeer }
955+ onClick = { isGroup ? openGroupDetails : undefined }
956+ style = { isGroup ? { cursor : "pointer" } : undefined }
957+ />
836958 < ConversationHeader . Content
837959 userName = { isGroup ? (
838960 < Box
0 commit comments