[stable-25.0.x] 👑 feat(owners): Allow to promote/demote users to/from owners - #6575
Merged
Conversation
backportbot
Bot
force-pushed
the
backport/6560/stable-25.0.x
branch
from
August 25, 2026 14:02
2627a2d to
571a8f2
Compare
Follow the web client and replace the textual "(moderator)" badge in the participants list with an icon: a crown for the conversation owner, a shield for moderators and guest moderators. See nextcloud/spreed#18924. The icons are Material Symbols "crown" and "shield", taken from the official Android exports in google/material-design-icons so no path data is hand-converted, and named after the outline_*_24 files already in the project. Ranks are only marked in conversations that have ranks. One-to-one, former one-to-one and changelog conversations are excluded, mirroring the web client: both participants of a one-to-one are owners by design. That also fixes the existing oddity of labelling both people in a one-to-one "(Moderator)". The role is resolved once in the ViewModel and carried on ParticipantModel rather than computed during composition, so it costs nothing per recomposition and ParticipantItemRow keeps its parameter list short. Owners now sort above moderators, and the participants comparator moved into the companion object so the ordering is directly testable. The icon is the only carrier of the role, so its contentDescription is the role label and is structurally non-null: the icon renders only when both the icon and the label resource resolve, so it can never degrade into a decorative icon that TalkBack skips. Offline dimming is now row-wide. The display name swapped emphasis colours and the avatar dropped to PARTICIPANT_OFFLINE_ALPHA, but everything else stayed at full opacity. A single contentAlpha derived in ParticipantItemRow now feeds the avatar, the role badge, the type label and the status row, so an offline participant no longer shows a dimmed avatar next to a full-strength crown. nc_owner and nc_moderator get translator hints, mirroring the TRANSLATORS comments the web client added alongside the same icons. Those two strings are now the only textual form of the role, used as the icon contentDescription, so a translator sees a bare "Owner" or "Moderator" with no surrounding sentence to disambiguate gender or register. Assisted-by: ClaudeCode:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 072ddc5)
…tomSheet The participant actions sheet was the last MaterialDialog bottom sheet in the conversation info screen. Bring it in line with the conversation list, following ConversationOperationsSheet as the reference. This drops the old index arithmetic, which built a fixed item list, removed entries by position and then incremented the tapped index back over the removals to work out which action was meant - so any new row silently rewired the ones below it. Actions are now a typed ParticipantOpsAction and row visibility lives in computeVisibility(). The sheet is state-driven via ConversationInfoUiState.participantForOps instead of assembling dialogs. The rebuilt header also shows the participant's role under the display name. Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit ec1cce0)
… for the participant action bottom sheet Assisted-by: Claude Code:claude-opus-5[1m] Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 8f8ed27)
Talk 23 lets owners hand the owner rank to another participant and take it back, through a participantType parameter on the existing moderators endpoint and two new system messages. See nextcloud/spreed#18924. Add the pieces the client needs before any of it can be offered in the UI: - the promote-demote-owner capability - an optional participantType query parameter on promoteAttendeeToModerator and demoteAttendeeFromModerator - OWNER_PROMOTED and OWNER_DEMOTED system message types Retrofit omits a null @query, so the existing calls pass null and keep the legacy "just toggle the moderator level" behaviour against every server, including ones without the capability. Without the two system message types the server's owner_promoted and owner_demoted fell through to DUMMY. The messages always rendered, since their text comes from the server, so this is about not discarding the type rather than fixing a visible break. No behaviour change: nothing calls the new parameter yet. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit de05d10)
Add the three ownership rows to the participant bottom sheet, matching the web client (nextcloud/spreed#18924): promote to owner, demote from owner to moderator, and demote from owner to participant. Icons follow the existing rule that a row shows the rank it leads to, so this is where the crown drawable finally gets used. All three sit behind canChangeOwnership(), which mirrors the server's own preconditions: the promote-demote-owner capability, the acting user being an owner, a group or public conversation whose object type does not imply an owner of its own, and a target that is a real user - guests, emails, federated users, phones and bots can never be owners. Self-demotion is deliberately asymmetric, as on the server: an owner may step down to moderator but not straight to participant, so they cannot lock themselves out of their own conversation. The participant.type == OWNER early return in handleParticipantClick had to go, or the sheet could never open for an owner at all. The rule it enforced - that an owner cannot be removed from the conversation - moves into the sheet's visibility rules, where the rest of the row logic already lives. Two Android specifics. USER_FOLLOWING_LINK is this client's name for the server's USER_SELF_JOINED, so it belongs in the promotable set. And ConversationEnums.ObjectType has no CLASSIFIED_PERSIST or EXTENDED_CONVERSATION and decodes unknown types to DEFAULT, so the client can offer a row the server will refuse; the following commit makes that refusal visible. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 41cd0e1)
…used participantActionObserver logged the error and dropped it, so a refusal was indistinguishable from success: the sheet closed and nothing happened. That mattered little while the only actions were promote and demote to moderator, which a moderator can always perform. With the ownership rows it does: the client cannot fully reproduce the server's preconditions, so it can legitimately offer a row the server then refuses. Read the reason the moderators endpoint reports in the OCS data and show it. The last-moderator case gets its own message, since it is the only refusal a moderator can plausibly hit by accident. Every other reason - room-type, actor-type, participant-type - means the client offered a row it should not have, and a generic failure is the honest answer for a client bug. Parsing is defensive: a malformed, empty or missing body falls back to the generic message rather than throwing, and the read is guarded because errorBody().string() can fail as readily as the JSON parse. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 8e444b5)
The rules deciding whether ownership can be handed over were private helpers in the Compose sheet, so nothing tested them. They are the kind of thing that rots quietly: a conversation type added to one of the sets, or a precondition the server tightens, changes who sees a destructive action with nothing to catch it. Move them to ParticipantRoleUtils, next to roleOf(), and test them there. canChangeOwnership() is the gate; canBePromotedToOwner() and canBeDemotedFromOwner() add the target's own rank. The sheet keeps only the question of which row to draw. The tests walk each dimension of the matrix rather than sampling it, so a new conversation or object type has to be classified deliberately instead of inheriting whatever the enum ordering gives it: every rankless conversation type, every object type with an implicit owner, every actor type that can never hold the rank, and every participant rank on both sides of promotable. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 091801d)
The participants list carries owner and moderator as an icon only. Anyone without moderation rights had no way to read it: the row was clickable but inert, so the rank had no text form for them anywhere in the app. Open the sheet for everyone. Without moderation rights it renders the header - display name and role - and no action rows, which is exactly the information the icon was withholding. The attendee PIN stays hidden as well; it is a SIP credential the server only hands to moderators. Hiding rows is presentation, not access control, so the canModerate() check does not simply disappear into the sheet: it moves to handleParticipantOpsAction, where it guards the API call itself rather than the drawing of a button. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de> (cherry picked from commit 7bd99fc)
In the participant actions sheet, the current user's own row shows their attendee PIN via the same `remove` slot used for actual remove/leave actions, so tapping the PIN text fired ParticipantOpsAction.RemoveFromConversation and removed the user from the conversation. As a result i got: 2026-08-25 13:49:51.393 23038-23038 Conversati...oViewModel com.nextcloud.talk2 E Error loading participants retrofit2.adapter.rxjava2.HttpException: HTTP 404 Every other branch of computeVisibility() already renders its PIN through infoPin, a plain non-interactive row. Route the self row's PIN through infoPin as well instead of wrapping it in a clickable RemoveOption. Signed-off-by: Marcel Hibbe <dev@mhibbe.de> (cherry picked from commit 4613dd3)
ParticipantOperationsSheetOwnerPreview passed conversation = null and spreedCapabilities = null, so computeVisibility() always took the !canModerate early return and rendered the same header-only state as ParticipantOperationsSheetInformationalPreview - the two previews were indistinguishable despite different names. Add previewConversation()/previewCapabilities() and pass a group conversation the previewing user owns, with the promote-demote-owner capability, so the preview actually exercises the demote-owner-to-moderator and demote-owner-to-user rows it is meant to show. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de> (cherry picked from commit bb1e933)
AndyScherzinger
force-pushed
the
backport/6560/stable-25.0.x
branch
from
August 25, 2026 14:52
571a8f2 to
9bb9b1d
Compare
AndyScherzinger
marked this pull request as ready for review
August 25, 2026 14:58
mahibi
approved these changes
Aug 25, 2026
Contributor
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/32862233397/artifacts/9571438498 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #6560
Warning, This backport's changes differ from the original and might be incomplete⚠️
Todo
Learn more about backports at https://docs.nextcloud.com/server/stable/go.php?to=developer-backports.