Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
402d395
feat(permissions): Split chat and reaction permissions
luflow Jan 23, 2026
7eaad47
docs: Update API documentation for REACT permission
luflow Jan 23, 2026
3ea09cb
docs: Apply suggestion from @nickvergessen
luflow Jan 26, 2026
0362fbe
feat(capabilities): Add react-permission capability
luflow Jan 26, 2026
70180bb
feat(frontend): Add federation fallback for react permission
luflow Jan 26, 2026
b4ccf0f
test(integration): Update expected participantPermissions to 510
luflow Jan 26, 2026
237a0a8
docs: Add react-permission integration guide for clients
luflow Jan 26, 2026
4e17361
feat(capabilities): Expose permissions max values via API
luflow Jan 28, 2026
32329ec
feat(frontend): Add capability-aware permissions UI with API fallback
luflow Jan 28, 2026
4c1b7b3
docs: Document permissions config and improve variable naming
luflow Jan 28, 2026
8f344e4
fix(openapi): Update permissions max value to 511
luflow Jan 28, 2026
b9d4b16
fix: correct handling of disabled states if canPostMessages is not al…
luflow Jan 28, 2026
bb7dc24
fix: Correct disabled behavior for audio recorder button
luflow Jan 28, 2026
a076c11
Merge branch 'main' into feat/split-chat-react-permissions
luflow Jan 28, 2026
8501a4a
fix: Address PR review feedback for permissions feature
luflow Jan 29, 2026
8353c1e
fix(chat): correctly update last message and unread counter from polling
Antreesy Jan 28, 2026
590c11f
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 29, 2026
944549f
fix(bots): Add reactions from bots before relaying the message
nickvergessen Jan 29, 2026
23a0083
test: Skip federation permissions test for older server versions
luflow Jan 29, 2026
f902d13
fix(permissions): Regenerate OpenAPI specs and update CapabilitiesTest
luflow Jan 29, 2026
69c0568
fix(permissions): Add permissions to LOCAL_CONFIGS array
luflow Jan 29, 2026
a34201b
fix(permissions): Allow empty list in config-local type definition
luflow Jan 29, 2026
0efce34
fix(permissions): Regenerate OpenAPI specs after type change
luflow Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,8 @@
* `scheduled-messages` (local) - Whether a user can schedule messages
* `config => call => live-translation` - Whether live translation is supported in calls
* `config => call => live-transcription-target-language-id` (local) - User defined string value with the id of the target language to use for live translations

## 24
* `react-permission` - When permission 256 is required to add reactions (previously handled by the chat permission)
* `config => permissions => max-default` - Maximum value for default permissions (510 with react-permission, 254 without)
* `config => permissions => max-custom` - Maximum value for custom permissions (511 with react-permission, 255 without)
3 changes: 2 additions & 1 deletion docs/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
* `16` Can publish audio stream
* `32` Can publish video stream
* `64` Can publish screen sharing stream
* `128` Can post chat message, share items and do reactions
* `128` Can post chat message and share items
* `256` Can add reactions (New in Nextcloud 34, previously handled by the chat permission)

### Attendee permission modifications
* `set` - Setting this permission set.
Expand Down
56 changes: 56 additions & 0 deletions docs/react-permission-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# React Permission Integration Guide

This document describes the changes needed for frontend clients to support the new separate reaction permission introduced in Talk 24 (Nextcloud 34).

## Background

Previously, the CHAT permission (128) controlled both posting messages and adding reactions. This has been split into two separate permissions:

- **CHAT (128)**: Post messages and share items
- **REACT (256)**: Add/remove reactions

## New Capability

A new capability `react-permission` indicates server support for the separate reaction permission.

```
features: [..., "react-permission", ...]
```

## Permission Constants

```
PERMISSIONS_REACT = 256
PERMISSIONS_MAX_DEFAULT = 510 // was 254
PERMISSIONS_MAX_CUSTOM = 511 // was 255
```

## Implementation

### Checking if user can react

```javascript
const hasReactPermissionCapability = capabilities.features.includes('react-permission')

const permissionToCheck = hasReactPermissionCapability
? PERMISSIONS_REACT // 256
: PERMISSIONS_CHAT // 128 (fallback for older servers)

const canReact = (participant.permissions & permissionToCheck) !== 0
```

### Why the fallback is needed

When federating with older Nextcloud servers or when the desktop client connects to an older server, the `react-permission` capability won't be present. In this case, clients should fall back to checking the CHAT permission, as that's what controlled reactions before this change.

## Migration

The backend migration automatically grants the REACT permission to all users who previously had the CHAT permission, ensuring backward compatibility.

## UI Changes

The permissions editor should show two separate checkboxes:
- "Can post messages" (CHAT - 128)
- "Can add reactions" (REACT - 256)

Instead of the previous combined "Can post messages and reactions" checkbox.
4 changes: 3 additions & 1 deletion docs/reaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`: since Nextcloud 24
+ `200 OK` Reaction already exists
+ `201 Created` User reacted with a new reaction
+ `400 Bad Request` In case of no reaction support, message out of reactions context or any other error
+ `403 Forbidden` When the participant does not have the required permission to react (see attendee permission `256`)
+ `404 Not Found` When the conversation or message to react could not be found for the participant

- Data:
Expand All @@ -45,8 +46,9 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`: since Nextcloud 24

* Response:
- Status code:
+ `201 Created`
+ `200 OK`
+ `400 Bad Request` In case of no reaction support, message out of reactions context or any other error
+ `403 Forbidden` When the participant does not have the required permission to react (see attendee permission `256`)
+ `404 Not Found` When the conversation or message to react or reaction could not be found for the participant

- Data:
Expand Down
11 changes: 11 additions & 0 deletions l10n/hr.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ OC.L10N.register(
"A TURN server is used to proxy the traffic from participants behind a firewall. If individual participants cannot connect to others a TURN server is most likely required. See {linkstart}this documentation{linkend} for setup instructions." : "Poslužitelj TURN upotrebljava se za usmjeravanje prometa sudionika iza vatrozida. Ako se pojedini sudionici ne mogu povezati s drugim sudionicima, vjerojatno je potreban poslužitelj TURN. Upute za postavljanje poslužitelja možete pronaći u {linkstart}ovoj dokumentaciji{linkend}.",
"TURN servers" : "Poslužitelji TURN",
"OK" : "U redu",
"Federated user" : "Federirani korisnik",
"Confirm" : "Potvrdi",
"Reset" : "Resetiraj",
"Cancel" : "Odustani",
Expand All @@ -622,6 +623,7 @@ OC.L10N.register(
"Save" : "Spremi",
"Search participants" : "Pretraži sudionike",
"No results" : "Nema rezultata",
"Done" : "Gotovo",
"Raise hand" : "Podigni ruku",
"Raise hand (R)" : "Podigni ruku (R)",
"Lower hand" : "Spusti ruku",
Expand Down Expand Up @@ -670,6 +672,7 @@ OC.L10N.register(
"Post message" : "Objavi poruku",
"Favorite" : "Favorit",
"Date:" : "Datum:",
"Note:" : "Napomena:",
"Hide details" : "Sakrij pojedinosti",
"Show details" : "Prikaži pojedinosti",
"Error while updating conversation description" : "Pogreška pri ažuriranju opisa razgovora",
Expand Down Expand Up @@ -768,6 +771,7 @@ OC.L10N.register(
"No microphone available" : "Nema dostupnih mikrofona",
"Select camera" : "Odaberi kameru",
"No camera available" : "Nema dostupnih kamera",
"Select a device" : "Odaberi uređaj",
"Test" : "Ispitivanje",
"Devices" : "Uređaji",
"No audio" : "Nema zvuka",
Expand All @@ -778,16 +782,19 @@ OC.L10N.register(
"Error while accessing microphone" : "Došlo je do pogreške pri pristupanju mikrofonu",
"Access to camera is only possible with HTTPS" : "Pristup kameri moguć je samo putem HTTPS-a",
"Invalid path selected" : "Odabran nevažeći put",
"Blur" : "Zamućenje",
"Upload" : "Otpremi",
"Files" : "Datoteke",
"24 hours" : "24 sata",
"Reply" : "Odgovori",
"More actions" : "Dodatne radnje",
"Set reminder" : "Postavi podsjetnik",
"Reply privately" : "Odgovori privatno",
"Copy message link" : "Kopiraj poveznicu poruke",
"Go to file" : "Idi na datoteku",
"Forward message" : "Proslijedi poruku",
"Translate" : "Prevedi",
"Set custom reminder" : "Postavi prilagođeni podsjetnik",
"Choose a conversation to forward the selected message." : "Odaberite razgovor u koji ćete proslijediti odabranu poruku.",
"Error while forwarding message" : "Pogreška pri prosljeđivanju poruke",
"The message has been forwarded to {selectedConversationName}" : "Poruka je proslijeđena u {selectedConversationName}",
Expand Down Expand Up @@ -1057,6 +1064,10 @@ OC.L10N.register(
"Join »%s«" : "Pridruži se »%s«",
"Always show the device preview screen before joining a call in this conversation." : "Uvijek prikažite zaslon za pretpregled uređaja prije pridruživanja pozivu u ovom razgovoru.",
"Talk settings" : "Postavke razgovora",
"Set reminder for later today" : "Postavi podsjetnik za kasnije danas",
"Set reminder for tomorrow" : "Postavi podsjetnik za sutra",
"Set reminder for this weekend" : "Postavi podsjetnik za ovaj vikend",
"Set reminder for next week" : "Postavi podsjetnik na sljedeći tjedan",
"Today" : "Danas",
"Yesterday" : "Jučer",
"_%n day ago_::_%n days ago_" : ["Prije %n dana","Prije %n dana","Prije %n dana"],
Expand Down
11 changes: 11 additions & 0 deletions l10n/hr.json
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@
"A TURN server is used to proxy the traffic from participants behind a firewall. If individual participants cannot connect to others a TURN server is most likely required. See {linkstart}this documentation{linkend} for setup instructions." : "Poslužitelj TURN upotrebljava se za usmjeravanje prometa sudionika iza vatrozida. Ako se pojedini sudionici ne mogu povezati s drugim sudionicima, vjerojatno je potreban poslužitelj TURN. Upute za postavljanje poslužitelja možete pronaći u {linkstart}ovoj dokumentaciji{linkend}.",
"TURN servers" : "Poslužitelji TURN",
"OK" : "U redu",
"Federated user" : "Federirani korisnik",
"Confirm" : "Potvrdi",
"Reset" : "Resetiraj",
"Cancel" : "Odustani",
Expand All @@ -620,6 +621,7 @@
"Save" : "Spremi",
"Search participants" : "Pretraži sudionike",
"No results" : "Nema rezultata",
"Done" : "Gotovo",
"Raise hand" : "Podigni ruku",
"Raise hand (R)" : "Podigni ruku (R)",
"Lower hand" : "Spusti ruku",
Expand Down Expand Up @@ -668,6 +670,7 @@
"Post message" : "Objavi poruku",
"Favorite" : "Favorit",
"Date:" : "Datum:",
"Note:" : "Napomena:",
"Hide details" : "Sakrij pojedinosti",
"Show details" : "Prikaži pojedinosti",
"Error while updating conversation description" : "Pogreška pri ažuriranju opisa razgovora",
Expand Down Expand Up @@ -766,6 +769,7 @@
"No microphone available" : "Nema dostupnih mikrofona",
"Select camera" : "Odaberi kameru",
"No camera available" : "Nema dostupnih kamera",
"Select a device" : "Odaberi uređaj",
"Test" : "Ispitivanje",
"Devices" : "Uređaji",
"No audio" : "Nema zvuka",
Expand All @@ -776,16 +780,19 @@
"Error while accessing microphone" : "Došlo je do pogreške pri pristupanju mikrofonu",
"Access to camera is only possible with HTTPS" : "Pristup kameri moguć je samo putem HTTPS-a",
"Invalid path selected" : "Odabran nevažeći put",
"Blur" : "Zamućenje",
"Upload" : "Otpremi",
"Files" : "Datoteke",
"24 hours" : "24 sata",
"Reply" : "Odgovori",
"More actions" : "Dodatne radnje",
"Set reminder" : "Postavi podsjetnik",
"Reply privately" : "Odgovori privatno",
"Copy message link" : "Kopiraj poveznicu poruke",
"Go to file" : "Idi na datoteku",
"Forward message" : "Proslijedi poruku",
"Translate" : "Prevedi",
"Set custom reminder" : "Postavi prilagođeni podsjetnik",
"Choose a conversation to forward the selected message." : "Odaberite razgovor u koji ćete proslijediti odabranu poruku.",
"Error while forwarding message" : "Pogreška pri prosljeđivanju poruke",
"The message has been forwarded to {selectedConversationName}" : "Poruka je proslijeđena u {selectedConversationName}",
Expand Down Expand Up @@ -1055,6 +1062,10 @@
"Join »%s«" : "Pridruži se »%s«",
"Always show the device preview screen before joining a call in this conversation." : "Uvijek prikažite zaslon za pretpregled uređaja prije pridruživanja pozivu u ovom razgovoru.",
"Talk settings" : "Postavke razgovora",
"Set reminder for later today" : "Postavi podsjetnik za kasnije danas",
"Set reminder for tomorrow" : "Postavi podsjetnik za sutra",
"Set reminder for this weekend" : "Postavi podsjetnik za ovaj vikend",
"Set reminder for next week" : "Postavi podsjetnik na sljedeći tjedan",
"Today" : "Danas",
"Yesterday" : "Jučer",
"_%n day ago_::_%n days ago_" : ["Prije %n dana","Prije %n dana","Prije %n dana"],
Expand Down
8 changes: 8 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Talk;

use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\LiveTranscriptionService;
use OCP\App\IAppManager;
use OCP\AppFramework\Services\IAppConfig;
Expand Down Expand Up @@ -78,6 +79,7 @@ class Capabilities implements IPublicCapability {
'rich-object-delete',
'unified-search',
'chat-permission',
'react-permission',
'silent-send',
'silent-call',
'send-call-notification',
Expand Down Expand Up @@ -202,6 +204,8 @@ class Capabilities implements IPublicCapability {
'experiments' => [
'enabled',
],
'permissions' => [
],
];

protected ICache $talkCache;
Expand Down Expand Up @@ -294,6 +298,10 @@ public function getCapabilities(): array {
'experiments' => [
'enabled' => max(0, $this->appConfig->getAppValueInt($user instanceof IUser ? 'experiments_users' : 'experiments_guests')),
],
'permissions' => [
'max-default' => Attendee::PERMISSIONS_MAX_DEFAULT,
'max-custom' => Attendee::PERMISSIONS_MAX_CUSTOM,
],
],
'config-local' => self::LOCAL_CONFIGS,
'version' => $this->appManager->getAppVersion('spreed'),
Expand Down
2 changes: 1 addition & 1 deletion lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public function isNotAllowedToCreateConversations(IUser $user): bool {
}

/**
* @return int<0, 255>
* @return int<0, 511>
* @psalm-return int-mask-of<Attendee::PERMISSIONS_*>
*/
public function getDefaultPermissions(): int {
Expand Down
4 changes: 2 additions & 2 deletions lib/Controller/ReactionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function __construct(
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequirePermission(permission: RequirePermission::REACT)]
#[RequireReadWriteConversation]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/reaction/{token}/{messageId}', requirements: [
Expand Down Expand Up @@ -108,7 +108,7 @@ public function react(int $messageId, string $reaction): DataResponse {
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequirePermission(permission: RequirePermission::REACT)]
#[RequireReadWriteConversation]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/reaction/{token}/{messageId}', requirements: [
Expand Down
8 changes: 4 additions & 4 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ protected function formatRoom(
* @psalm-param non-negative-int|null $lobbyTimer
* @param 0|1|2 $sipEnabled Whether SIP dial-in shall be enabled (only available with `conversation-creation-all` capability)
* @psalm-param Webinary::SIP_* $sipEnabled
* @param int<0, 255> $permissions Default permissions for participants (only available with `conversation-creation-all` capability)
* @param int<0, 511> $permissions Default permissions for participants (only available with `conversation-creation-all` capability)
* @psalm-param int-mask-of<Attendee::PERMISSIONS_*> $permissions
* @param 0|1 $recordingConsent Whether participants need to agree to a recording before joining a call (only available with `conversation-creation-all` capability)
* @psalm-param RecordingService::CONSENT_REQUIRED_NO|RecordingService::CONSENT_REQUIRED_YES $recordingConsent
Expand Down Expand Up @@ -2665,7 +2665,7 @@ protected function changeParticipantType(int $attendeeId, bool $promote): DataRe
* Update the permissions of a room
*
* @param 'call'|'default' $mode Level of the permissions ('call' (removed in Talk 20), 'default')
* @param int<0, 255> $permissions New permissions
* @param int<0, 511> $permissions New permissions
* @psalm-param int-mask-of<Attendee::PERMISSIONS_*> $permissions
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'mode'|'type'|'value'}, array{}>
*
Expand Down Expand Up @@ -2699,7 +2699,7 @@ public function setPermissions(string $mode, int $permissions): DataResponse {
* @param int $attendeeId ID of the attendee
* @psalm-param non-negative-int $attendeeId
* @param 'set'|'remove'|'add' $method Method of updating permissions ('set', 'remove', 'add')
* @param int<0, 255> $permissions New permissions
* @param int<0, 511> $permissions New permissions
* @psalm-param int-mask-of<Attendee::PERMISSIONS_*> $permissions
* @return DataResponse<Http::STATUS_OK, list<TalkParticipant>, array{X-Nextcloud-Has-User-Statuses?: true}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'participant'|'method'|'moderator'|'room-type'|'type'|'value'}, array{}>
*
Expand Down Expand Up @@ -2738,7 +2738,7 @@ public function setAttendeePermissions(int $attendeeId, string $method, int $per
*
* @param 'set'|'remove'|'add' $method Method of updating permissions ('set', 'remove', 'add')
* @psalm-param Attendee::PERMISSIONS_MODIFY_* $method
* @param int<0, 255> $permissions New permissions
* @param int<0, 511> $permissions New permissions
* @psalm-param int-mask-of<Attendee::PERMISSIONS_*> $permissions
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, null, array{}>
* @deprecated Call permissions have been removed
Expand Down
1 change: 1 addition & 0 deletions lib/Middleware/Attribute/RequirePermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
class RequirePermission {

public const CHAT = 'chat';
public const REACT = 'react';
public const START_CALL = 'call-start';

public function __construct(
Expand Down
3 changes: 3 additions & 0 deletions lib/Middleware/InjectionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ protected function checkPermission(AEnvironmentAwareOCSController $controller, s
if ($permission === RequirePermission::CHAT && !($participant->getPermissions() & Attendee::PERMISSIONS_CHAT)) {
throw new PermissionsException();
}
if ($permission === RequirePermission::REACT && !($participant->getPermissions() & Attendee::PERMISSIONS_REACT)) {
throw new PermissionsException();
}
if ($permission === RequirePermission::START_CALL && !($participant->getPermissions() & Attendee::PERMISSIONS_CALL_START)) {
throw new PermissionsException();
}
Expand Down
Loading
Loading