Skip to content

Commit d8f212a

Browse files
authored
Merge pull request #2366 from luflow/feat/react-permission-capability
feat: Add support for separate react permission (Talk 24+)
2 parents 818b66a + fdcc2aa commit d8f212a

10 files changed

Lines changed: 137 additions & 16 deletions

File tree

NextcloudTalk/Chat/BaseChatViewController.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,6 +2886,11 @@ import SwiftUI
28862886
// MARK: - Reactions
28872887

28882888
func addReaction(reaction: String, to message: NCChatMessage) {
2889+
if !self.room.canReact {
2890+
NotificationPresenter.shared().present(text: NSLocalizedString("You are not allowed to add or remove reactions in this conversation", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
2891+
return
2892+
}
2893+
28892894
if message.reactionsArray().contains(where: { $0.reaction == reaction && $0.userReacted }) {
28902895
// We can't add reaction twice
28912896
return
@@ -2908,6 +2913,11 @@ import SwiftUI
29082913
}
29092914

29102915
func removeReaction(reaction: String, from message: NCChatMessage) {
2916+
if !self.room.canReact {
2917+
NotificationPresenter.shared().present(text: NSLocalizedString("You are not allowed to add or remove reactions in this conversation", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
2918+
return
2919+
}
2920+
29112921
self.setTemporaryReaction(reaction: reaction, withState: .removing, toMessage: message)
29122922

29132923
NCAPIController.sharedInstance().removeReaction(reaction, fromMessage: message.messageId, inRoom: self.room.token, for: self.account) { _, error, _ in

NextcloudTalk/Chat/ChatViewController.swift

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ import SwiftUI
787787

788788
// Disable call buttons
789789
self.callOptionsButton.isEnabled = false
790-
} else if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room), !room.permissions.contains(.chat) {
790+
} else if !room.canChat {
791791
// Hide text input
792792
self.setTextInputbarHidden(true, animated: isVisible)
793793
} else if self.isTextInputbarHidden {
@@ -2279,9 +2279,7 @@ import SwiftUI
22792279
}
22802280

22812281
override func getContextMenuAccessoryView(forMessage message: NCChatMessage, forIndexPath indexPath: IndexPath, withCellHeight cellHeight: CGFloat) -> UIView? {
2282-
let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || self.room.permissions.contains(.chat)
2283-
2284-
guard hasChatPermissions && self.isMessageReactable(message: message) else { return nil }
2282+
guard self.room.canReact && self.isMessageReactable(message: message) else { return nil }
22852283

22862284
let reactionViewPadding = 10
22872285
let emojiButtonPadding = 10
@@ -2400,7 +2398,6 @@ import SwiftUI
24002398

24012399
var actions: [UIMenuElement] = []
24022400
var informationalActions: [UIMenuElement] = []
2403-
let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || self.room.permissions.contains(.chat)
24042401

24052402
// Show edit information
24062403
if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 {
@@ -2425,14 +2422,14 @@ import SwiftUI
24252422
}
24262423

24272424
// Reply option
2428-
if self.isMessageReplyable(message: message), hasChatPermissions, !self.textInputbar.isEditing {
2425+
if self.isMessageReplyable(message: message), self.room.canChat, !self.textInputbar.isEditing {
24292426
actions.append(UIAction(title: NSLocalizedString("Reply", comment: ""), image: .init(systemName: "arrowshape.turn.up.left")) { _ in
24302427
self.didPressReply(for: message)
24312428
})
24322429
}
24332430

24342431
// Show "Add reaction" when running on MacOS because we don't have an accessory view
2435-
if self.isMessageReactable(message: message), hasChatPermissions, NCUtils.isiOSAppOnMac() {
2432+
if self.isMessageReactable(message: message), self.room.canReact, NCUtils.isiOSAppOnMac() {
24362433
actions.append(UIAction(title: NSLocalizedString("Add reaction", comment: ""), image: .init(systemName: "face.smiling")) { _ in
24372434
self.didPressAddReaction(for: message, at: indexPath)
24382435
})
@@ -2506,7 +2503,7 @@ import SwiftUI
25062503
}
25072504

25082505
// Re-send option
2509-
if (message.sendingFailed || message.isOfflineMessage) && hasChatPermissions {
2506+
if (message.sendingFailed || message.isOfflineMessage) && self.room.canChat {
25102507
actions.append(UIAction(title: NSLocalizedString("Resend", comment: ""), image: .init(systemName: "arrow.clockwise")) { _ in
25112508
self.didPressResend(for: message)
25122509
})
@@ -2577,14 +2574,14 @@ import SwiftUI
25772574
var destructiveMenuActions: [UIMenuElement] = []
25782575

25792576
// Edit option
2580-
if message.isEditable(for: self.account, in: self.room) && hasChatPermissions {
2577+
if message.isEditable(for: self.account, in: self.room) && self.room.canChat {
25812578
destructiveMenuActions.append(UIAction(title: NSLocalizedString("Edit", comment: "Edit a message or room participants"), image: .init(systemName: "pencil")) { _ in
25822579
self.didPressEdit(for: message)
25832580
})
25842581
}
25852582

25862583
// Delete option
2587-
if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: self.account, in: self.room) && hasChatPermissions) {
2584+
if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: self.account, in: self.room) && self.room.canChat) {
25882585
destructiveMenuActions.append(UIAction(title: NSLocalizedString("Delete", comment: ""), image: .init(systemName: "trash"), attributes: .destructive) { _ in
25892586
self.didPressDelete(for: message)
25902587
})

NextcloudTalk/Database/NCDatabaseManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ extern NSString * const kCapabilitySensitiveConversations;
8787
extern NSString * const kCapabilityThreads;
8888
extern NSString * const kCapabilityPinnedMessages;
8989
extern NSString * const kCapabilityScheduleMessages;
90+
extern NSString * const kCapabilityReactPermission;
9091

9192
extern NSString * const kNotificationsCapabilityExists;
9293
extern NSString * const kNotificationsCapabilityTestPush;

NextcloudTalk/Database/NCDatabaseManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
NSString * const kCapabilityThreads = @"threads";
8989
NSString * const kCapabilityPinnedMessages = @"pinned-messages";
9090
NSString * const kCapabilityScheduleMessages = @"scheduled-messages";
91+
NSString * const kCapabilityReactPermission = @"react-permission";
9192

9293
NSString * const kNotificationsCapabilityExists = @"exists";
9394
NSString * const kNotificationsCapabilityTestPush = @"test-push";

NextcloudTalk/NCTypes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ typedef NS_OPTIONS(NSInteger, NCPermission) {
9494
NCPermissionCanPublishVideo = 32,
9595
NCPermissionCanPublishScreen = 64,
9696
NCPermissionChat = 128,
97+
NCPermissionReact = 256,
9798
};
9899

99100
typedef NS_ENUM(NSInteger, NCMessageExpiration) {

NextcloudTalk/Rooms/NCRoom.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,5 +348,19 @@ import SwiftyAttributes
348348
return self.permissions.contains(.canPublishScreen) || !supportsConversationPermissions
349349
}
350350

351+
public var canChat: Bool {
352+
// For very old servers without chat-permission capability, allow chat
353+
return !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: self) || self.permissions.contains(.chat)
354+
}
355+
356+
public var canReact: Bool {
357+
// Check if server supports separate react permission (Talk 24+)
358+
if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityReactPermission, for: self) {
359+
return self.permissions.contains(.react)
360+
}
361+
362+
// Fallback for older servers: reactions were tied to chat permission
363+
return self.canChat
364+
}
351365

352366
}

NextcloudTalk/en.lproj/Localizable.strings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,6 +2468,9 @@
24682468
/* No comment provided by engineer. */
24692469
"You are currently waiting in the lobby" = "You are currently waiting in the lobby";
24702470

2471+
/* No comment provided by engineer. */
2472+
"You are not allowed to add or remove reactions in this conversation" = "You are not allowed to add or remove reactions in this conversation";
2473+
24712474
/* No comment provided by engineer. */
24722475
"You are not part of any conversation" = "You are not part of any conversation";
24732476

NextcloudTalkTests/UI/UIRoomTest.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,43 @@ final class UIRoomTest: XCTestCase {
284284
let textView = toolbar.textViews["Write message, @ to mention someone …"]
285285
XCTAssert(!textView.exists)
286286
}
287+
288+
func testReactOnlyPermission() throws {
289+
let app = launchAndLogin()
290+
291+
// ReactOnlyTest room is only created for Talk 24+ (main branch)
292+
let reactOnlyCell = app.tables.cells.staticTexts["ReactOnlyTest"]
293+
294+
// Skip test if the room doesn't exist (older server versions)
295+
try XCTSkipUnless(reactOnlyCell.waitForExistence(timeout: TestConstants.timeoutShort),
296+
"ReactOnlyTest room not found - skipping (requires Talk 24+)")
297+
298+
reactOnlyCell.tap()
299+
300+
let chatNavBar = app.navigationBars["NextcloudTalk.ChatView"]
301+
XCTAssert(chatNavBar.waitForExistence(timeout: TestConstants.timeoutLong))
302+
303+
// Find the message from alice that we should react to
304+
let messageText = app.tables.textViews["React to this message!"].firstMatch
305+
XCTAssert(messageText.waitForExistence(timeout: TestConstants.timeoutShort))
306+
307+
// Open context menu by long-pressing on the message
308+
messageText.press(forDuration: 2.0)
309+
310+
// Tap the thumbs up reaction from the context menu (same pattern as testDeallocation)
311+
let thumbsUpReaction = app.staticTexts["👍"]
312+
XCTAssert(thumbsUpReaction.waitForExistence(timeout: TestConstants.timeoutShort))
313+
thumbsUpReaction.tap()
314+
315+
// Verify the reaction was added - the reaction label shows "emoji count" format
316+
let reactionLabel = app.staticTexts["👍 1"]
317+
XCTAssert(reactionLabel.waitForExistence(timeout: TestConstants.timeoutShort),
318+
"Reaction should be visible after adding it")
319+
320+
// Verify that we cannot send messages (no chat permission)
321+
// The toolbar/text input should not be present when user cannot chat
322+
let toolbar = app.toolbars["Toolbar"]
323+
let textView = toolbar.textViews["Write message, @ to mention someone …"]
324+
XCTAssertFalse(textView.exists, "Text input should not exist when user cannot chat")
325+
}
287326
}

ShareExtension/ShareViewController.m

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
509509
return;
510510
}
511511

512-
BOOL hasChatPermission = ![[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatPermission] || (room.permissions & NCPermissionChat) != 0;
513-
514-
if (!hasChatPermission || room.readOnlyState == NCRoomReadOnlyStateReadOnly) {
512+
if (!room.canChat || room.readOnlyState == NCRoomReadOnlyStateReadOnly) {
515513
[self showChatPermissionAlert];
516514
return;
517515
}

ci-setup-rooms.sh

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@
55

66
# This script is intended to setup specific rooms that we want to test
77

8+
SERVER_URL="http://localhost:8080"
9+
10+
# Check if a Talk capability is available
11+
# Usage: has_capability "capability-name"
12+
has_capability() {
13+
local capability="$1"
14+
curl -s -u admin:admin "$SERVER_URL/ocs/v2.php/cloud/capabilities" \
15+
-H "OCS-APIRequest: true" \
16+
-H 'accept: application/json, text/plain, */*' \
17+
| jq -e ".ocs.data.capabilities.spreed.features | any(. == \"$capability\")" > /dev/null 2>&1
18+
}
19+
820
# Setup a room with lobby enabled and add admin as a normal participant
9-
response=$(curl -u alice:alice 'http://localhost:8080/ocs/v2.php/apps/spreed/api/v4/room' \
21+
response=$(curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room" \
1022
-H "OCS-APIRequest: true" \
1123
-H 'content-type: application/json' \
1224
-H 'accept: application/json, text/plain, */*' \
@@ -18,15 +30,60 @@ token=$(echo $response | jq -r .ocs.data.token)
1830

1931
echo $token
2032

21-
curl -u alice:alice "http://localhost:8080/ocs/v2.php/apps/spreed/api/v4/room/$token/webinar/lobby" \
33+
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/webinar/lobby" \
2234
-X 'PUT' \
2335
-H "OCS-APIRequest: true" \
2436
-H 'content-type: application/json' \
2537
-H 'accept: application/json, text/plain, */*' \
2638
--data-raw '{"state":1}'
2739

28-
curl -u alice:alice "http://localhost:8080/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
40+
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
2941
-H "OCS-APIRequest: true" \
3042
-H 'content-type: application/json' \
3143
-H 'accept: application/json, text/plain, */*' \
3244
--data-raw '{"newParticipant":"admin","source":"users"}'
45+
46+
# Setup a room with react-only permission (can react but cannot chat)
47+
# Only available when server supports react-permission capability (Talk 24+)
48+
if has_capability "react-permission"; then
49+
echo "Setting up ReactOnlyTest room (react-permission capability detected)"
50+
51+
# Permission values: CustomPermissions=1 (auto-added), JoinCall=4, React=256
52+
# Total: 260 (4 + 256, custom flag auto-added when non-zero)
53+
response=$(curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room" \
54+
-H "OCS-APIRequest: true" \
55+
-H 'content-type: application/json' \
56+
-H 'accept: application/json, text/plain, */*' \
57+
--data-raw '{"roomType":2,"roomName":"ReactOnlyTest"}')
58+
59+
echo $response
60+
61+
token=$(echo $response | jq -r .ocs.data.token)
62+
63+
echo "ReactOnlyTest token: $token"
64+
65+
# Set default permissions to react-only (no chat permission)
66+
# 260 = JoinCall(4) + React(256)
67+
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/permissions/default" \
68+
-X 'PUT' \
69+
-H "OCS-APIRequest: true" \
70+
-H 'content-type: application/json' \
71+
-H 'accept: application/json, text/plain, */*' \
72+
--data-raw '{"permissions":260}'
73+
74+
# Add admin as a participant (will get the default react-only permissions)
75+
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
76+
-H "OCS-APIRequest: true" \
77+
-H 'content-type: application/json' \
78+
-H 'accept: application/json, text/plain, */*' \
79+
--data-raw '{"newParticipant":"admin","source":"users"}'
80+
81+
# Send a message from alice (moderator) so there's something to react to
82+
curl -X POST -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v1/chat/$token" \
83+
-H "OCS-APIRequest: true" \
84+
-H 'content-type: application/json' \
85+
-H 'accept: application/json' \
86+
--data-raw '{"message":"React to this message!"}'
87+
else
88+
echo "Skipping ReactOnlyTest room setup (react-permission capability not available)"
89+
fi

0 commit comments

Comments
 (0)