+
Overview
+
+ {/* Hero icon */}
+
+
+
+
+ {/* Clear (X) button — owner-only, only when we have a non-empty icon to clear */}
+ {isOwner && previewIconUrl && (
+
+ )}
+
+
+
+
+
+ {/* Group name */}
+
+
+
setName(e.target.value.slice(0, MAX_NAME_LENGTH))}
+ placeholder={fallbackName || 'Group DM'}
+ disabled={!isOwner}
+ maxLength={MAX_NAME_LENGTH}
+ className="input-standard w-full"
+ data-group-dm-name-input
+ aria-label="Group name"
+ />
+ {isOwner && (
+
+ {trimmedName.length}/{MAX_NAME_LENGTH}
+
+ )}
+
+
+ {saveError && (
+
+ {saveError}
+
+ )}
+
+ {/* Save / Cancel — owner only, only when dirty */}
+ {isOwner && (
+
+
+
+
+ )}
+
+ {/* For non-owners: a single Close button (no Save). */}
+ {!isOwner && (
+
+
+
+ )}
+
+ {/* Leave Group — destructive footer button, everyone */}
+
+
+ Leave Group
+
+
+ You will stop receiving messages from this conversation. Other members will see a system message.
+
+
+
+
+ );
+
+ // ── Members panel ──────────────────────────────────────────────────────
+ const ownerMember = dmChannel.members.find((m) => m.id === dmChannel.ownerId) ?? null;
+ const sortByDisplayName = (a: User, b: User) => {
+ const aName = (a.displayName ?? parseFederatedUsername(a.username).baseName).toLowerCase();
+ const bName = (b.displayName ?? parseFederatedUsername(b.username).baseName).toLowerCase();
+ return aName.localeCompare(bName);
+ };
+ const nonOwnerMembers = dmChannel.members
+ .filter((m) => m.id !== dmChannel.ownerId)
+ .sort(sortByDisplayName);
+
+ const membersPanel = (
+
+
+
Members
+
+ {memberCount}/{MAX_GROUP_MEMBERS}
+
+
+
+
+
+
+ {ownerMember && (
+
+ )}
+ {nonOwnerMembers.map((m) => (
+
+ ))}
+
+
+ );
+
+ return (
+