diff --git a/web/src/features/groups/CreateGroupModal.tsx b/web/src/features/groups/CreateGroupModal.tsx index b6aacab3..e02d73ff 100644 --- a/web/src/features/groups/CreateGroupModal.tsx +++ b/web/src/features/groups/CreateGroupModal.tsx @@ -63,8 +63,21 @@ export function CreateGroupModal({ onClose, onCreated }: CreateGroupModalProps) setError(''); let result: Awaited>; try { + // The input's own placeholder promises "未填写时默认为成员昵称组合" (defaults to a + // combination of member nicknames when left blank), but `name` has no such fallback + // server-side (unlike `description`, which the protocol doc explicitly allows empty) -- + // confirmed live: submitting a blank name gets rejected outright with MALFORMED_REQUEST, not + // silently defaulted. Truncated the same way the input field itself is (MAX_TITLE_LENGTH), + // with an ellipsis when it had to cut names short, so this can't itself produce an + // over-length name the server would also reject. + const trimmedName = name.trim(); + let finalName = trimmedName; + if (!finalName) { + const joined = [...selected.values()].join('、'); + finalName = joined.length > MAX_TITLE_LENGTH ? `${joined.slice(0, MAX_TITLE_LENGTH - 1)}…` : joined; + } result = await createGroup({ - name: name.trim(), + name: finalName, member_user_ids: [...selected.keys()], }); } catch (err) { diff --git a/web/src/features/groups/GroupAdminModal.tsx b/web/src/features/groups/GroupAdminModal.tsx index 01865078..77d4bff0 100644 --- a/web/src/features/groups/GroupAdminModal.tsx +++ b/web/src/features/groups/GroupAdminModal.tsx @@ -10,7 +10,7 @@ import { getAllGroupMembers, getGroup, getGroupGovernance, - getGroupMutes, + getAllGroupMutes, getGroupRoleLimits, getGroupSummary, muteGroupMembers, @@ -106,7 +106,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { queryFn: () => getAllGroupMembers(topicId, { role: 'admin' }), enabled: isOwner, }); - const mutesQuery = useQuery({ queryKey: ['group-mutes', topicId], queryFn: () => getGroupMutes(topicId) }); + const mutesQuery = useQuery({ queryKey: ['group-mutes', topicId], queryFn: () => getAllGroupMutes(topicId) }); const govMutation = useMutation({ mutationFn: (patch: Parameters[1]) => updateGroupGovernance(topicId, patch), @@ -133,7 +133,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { onClose={() => setView('main')} /> ); - if (view === 'mutes') return setView('main')} />; + if (view === 'mutes') return setView('main')} />; if (view === 'transfer') return setView('main')} />; return ( @@ -153,7 +153,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { setGov({ join_approval: !govQuery.data?.join_approval })} /> @@ -164,7 +164,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { setGov({ mute_all: !govQuery.data?.mute_all })} /> @@ -177,7 +177,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'admin', label: '仅管理员' }, ]} onChange={(v) => setGov({ member_upload_files: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_upload_files_editable === false} />
@@ -189,7 +189,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'friends', label: '仅好友' }, ]} onChange={(v) => setGov({ member_private_chat: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_private_chat_editable === false} />
{/* `governance.member_invite`/`member_invite_editable` (docs/IM-前后端对接文档.md:1600) is a @@ -206,7 +206,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { { value: 'admin', label: '仅管理员' }, ]} onChange={(v) => setGov({ member_invite: v === 'all' })} - disabled={!govQuery.data || govMutation.isPending} + disabled={!govQuery.data || govMutation.isPending || govQuery.data?.member_invite_editable === false} /> {/* Owner-only, same as 转让群主 below -- the backend's own permission check for this @@ -226,7 +226,7 @@ export function GroupAdminModal({ topicId, onClose }: GroupAdminModalProps) { @@ -384,10 +384,20 @@ function AdminsSubView({ ))} - + {(() => { + const atLimit = !!maxAdminCount && admins.length >= maxAdminCount; + return ( + + ); + })()} )} setError('')} /> diff --git a/web/src/features/groups/GroupDetailColumn.tsx b/web/src/features/groups/GroupDetailColumn.tsx index f271fe2f..8aafb690 100644 --- a/web/src/features/groups/GroupDetailColumn.tsx +++ b/web/src/features/groups/GroupDetailColumn.tsx @@ -119,6 +119,11 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: // (no extra request needed) -- live-verified this reflects the real value (flipped it via // GroupAdminModal's own PATCH, re-fetched `/settings`, confirmed it changed), not a frozen default. const canInvite = isAdmin || settingsQuery.data?.governance?.member_invite === true; + // Same gap as `canInvite` right above (same governance toggle pattern, same root cause: the + // "群文件上传权限" toggle in GroupAdminModal writes `member_upload_files` but nothing ever read it + // back) -- `GroupFilesModal`'s upload button was unconditionally visible/clickable regardless of + // this setting. + const canUpload = isAdmin || settingsQuery.data?.governance?.member_upload_files === true; // No `version` in the request -- see updateGroupMemberSettings's own doc comment for why sending // one 400s every single call (this was the actual root cause of "member-settings always 400", @@ -159,7 +164,11 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: const clearHistory = async () => { if (!(await requestConfirm({ message: '确定清空本群的聊天记录吗?此操作仅清除本地记录,不影响其他成员。', danger: true }))) return; - tinode.getTopic(topicId).delMessagesAll(false); + try { + await tinode.getTopic(topicId).delMessagesAll(false); + } catch (err) { + showError(err, '清空聊天记录失败,请重试'); + } }; const reportAbuse = () => { @@ -411,7 +420,7 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: {openModal === 'announcement' ? ( setOpenModal(null)} /> ) : null} - {openModal === 'files' ? setOpenModal(null)} /> : null} + {openModal === 'files' ? setOpenModal(null)} /> : null} {openModal === 'admin' ? setOpenModal(null)} /> : null} {openModal === 'qrcode' ? setOpenModal(null)} /> : null} diff --git a/web/src/features/groups/GroupFilesModal.tsx b/web/src/features/groups/GroupFilesModal.tsx index 9cadb67d..5d75d7c1 100644 --- a/web/src/features/groups/GroupFilesModal.tsx +++ b/web/src/features/groups/GroupFilesModal.tsx @@ -34,6 +34,11 @@ interface GroupFilesModalProps { * owner/admin has no way to remove another member's upload from this UI even though the server * would accept the request. */ isAdmin: boolean; + /** `governance.member_upload_files` (所有成员/仅管理员), computed by the caller alongside + * `isAdmin` the exact same way `GroupDetailColumn`'s own `canInvite` already is -- without this, + * the toggle in GroupAdminModal had zero effect on this button, the same class of bug + * `canInvite` was already fixed for. */ + canUpload: boolean; onClose: () => void; } @@ -60,7 +65,7 @@ function fileTypeFromMime(mime: string, name: string): string { * more fields than the protocol doc's own minimal example (see groups.ts's header comment), ported * from the old app's real, working upload flow rather than guessed from the doc. */ -export function GroupFilesModal({ topicId, isAdmin, onClose }: GroupFilesModalProps) { +export function GroupFilesModal({ topicId, isAdmin, canUpload, onClose }: GroupFilesModalProps) { const queryClient = useQueryClient(); const myUserId = useSessionStore((s) => s.myUserId); const [query, setQuery] = useState(''); @@ -139,14 +144,19 @@ export function GroupFilesModal({ topicId, isAdmin, onClose }: GroupFilesModalPr 群文件 {files.length}
{/* Mock (#1h): accent-blue `upload` icon + "上传" label, not a bare icon-only button in - the neutral gray this used to be. */} - + the neutral gray this used to be. Hidden (not just disabled) when governance says + only admins may upload and this viewer isn't one -- there's no "ask an admin" action + a plain member could take from here, so a visible-but-blocked button would only + invite a confusing click. */} + {canUpload ? ( + + ) : null} diff --git a/web/src/lib/api/groups.ts b/web/src/lib/api/groups.ts index 7fb7e9c8..c1f875df 100644 --- a/web/src/lib/api/groups.ts +++ b/web/src/lib/api/groups.ts @@ -252,6 +252,14 @@ export function getGroupMutes(topic: string, cursor = '') { return imGet<{ items: GroupMute[]; next_cursor?: string }>(`/groups/${encodeURIComponent(topic)}/mutes${cursor ? `?cursor=${cursor}` : ''}`); } +/** `getGroupMutes` alone only ever reads the first page -- unlike `getAllGroupMembers`'s own + * `fetchAllPages` wrapper right above, GroupAdminModal's mutes view called the single-page + * function directly, silently truncating both the list and its own header count once a group has + * enough muted members to span more than one page. */ +export function getAllGroupMutes(topic: string) { + return fetchAllPages((cursor) => getGroupMutes(topic, cursor)); +} + export function unmuteGroupMember(topic: string, userId: string) { return imDelete<{ user_id: string; muted: boolean }>(`/groups/${encodeURIComponent(topic)}/mutes/${encodeURIComponent(userId)}`); }