Skip to content

Commit 723ee1a

Browse files
committed
feat: add group avatar updates
1 parent c9a7929 commit 723ee1a

5 files changed

Lines changed: 314 additions & 44 deletions

File tree

apps/messaging/src/components/ChatWindow.tsx

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import SlideInRight from "./transitions/SlideInRight";
3030
import { useColorMode } from "../contexts/ColorModeProvider";
3131
import { addMessagingContact } from "../utils/contacts";
3232
import {
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

apps/messaging/src/components/HomePage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export default function HomePage() {
286286
<ConversationList style={{ height: "auto", overflow: "visible" }}>
287287
{filteredGroupEntries.map(([groupId, group]) => {
288288
const groupName = group.name || groupId;
289-
const src = avatarDataUrl(groupId);
289+
const src = group.avatar ?? avatarDataUrl(groupId);
290290
const selected = activePeer === groupId;
291291
const unreadCnt = unreadBySender.get(groupId) ?? 0;
292292

0 commit comments

Comments
 (0)