Skip to content

👑 feat(owners): Allow to promote/demote users to/from owners - #6560

Merged
AndyScherzinger merged 9 commits into
masterfrom
feat/noid/owner-moderator
Aug 25, 2026
Merged

👑 feat(owners): Allow to promote/demote users to/from owners#6560
AndyScherzinger merged 9 commits into
masterfrom
feat/noid/owner-moderator

Conversation

@AndyScherzinger

@AndyScherzinger AndyScherzinger commented Aug 24, 2026

Copy link
Copy Markdown
Member

🖼️ Screenshots (dark mode)

🏚️ Before (24) 🏡 After (24) 🏡 After (25, diff account, test server, no translations yet)
Screenshot_20260824_142027 Screenshot_20260824_141837 Screenshot_20260824_183708

🚧 TODO

  • review, mere, backport, merge

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • x] 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@AndyScherzinger AndyScherzinger added this to the 25.1.0 milestone Aug 24, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/owner-moderator branch 2 times, most recently from 7ab80e3 to 3da4351 Compare August 24, 2026 16:12
@github-actions

Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/32749479098/artifacts/9528702592
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@AndyScherzinger
AndyScherzinger marked this pull request as ready for review August 24, 2026 16:38
@AndyScherzinger
AndyScherzinger requested a review from mahibi August 24, 2026 16:39
@AndyScherzinger

Copy link
Copy Markdown
Member Author

/backport to stable-25.0.x

@github-actions

Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/32765644301/artifacts/9534563292
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@AndyScherzinger AndyScherzinger changed the title 👑 feat(owners): Allow to promote/demote users to/from owners - #18924 👑 feat(owners): Allow to promote/demote users to/from owners Aug 25, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/owner-moderator branch from 91ffef7 to 28401e6 Compare August 25, 2026 10:52
@mahibi
mahibi force-pushed the feat/noid/owner-moderator branch from 28401e6 to 3d0a35e Compare August 25, 2026 12:49

@mahibi mahibi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added two commits. Ready to merge from my side

AndyScherzinger and others added 9 commits August 25, 2026 15:20
…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>
… for the participant action bottom sheet

Assisted-by: Claude Code:claude-opus-5[1m]
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
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>
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>
…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>
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>
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>
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>
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>
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/owner-moderator branch from 3d0a35e to bb1e933 Compare August 25, 2026 13:20
@AndyScherzinger
AndyScherzinger merged commit 06f4fb8 into master Aug 25, 2026
15 of 18 checks passed
@AndyScherzinger

Copy link
Copy Markdown
Member Author

/backport to stable-25.0.x

@backportbot

backportbot Bot commented Aug 25, 2026

Copy link
Copy Markdown

The backport to stable-25.0.x failed. Please do this backport manually.

# Switch to the target branch and update it
git checkout stable-25.0.x
git pull origin stable-25.0.x

# Create the new backport branch
git checkout -b backport/6560/stable-25.0.x

# Cherry pick the change from the commit sha1 of the change against the default branch
# This might cause conflicts, resolve them
git cherry-pick ec1cce0b 8f8ed279 de05d106 41cd0e16 8e444b50 091801dc 7bd99fcf 4613dd3f bb1e9332

# Push the cherry pick commit to the remote repository and open a pull request
git push origin backport/6560/stable-25.0.x

Error: Failed to create pull request: Validation Failed: {"resource":"PullRequest","code":"custom","message":"A pull request already exists for nextcloud:backport/6560/stable-25.0.x."} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request


Learn more about backports at https://docs.nextcloud.com/server/stable/go.php?to=developer-backports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants