Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions NextcloudTalk/Chat/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2803,6 +2803,11 @@ import SwiftUI
// MARK: - Reactions

func addReaction(reaction: String, to message: NCChatMessage) {
if !self.room.canReact {
NotificationPresenter.shared().present(text: NSLocalizedString("You are not allowed to add or remove reactions in this conversation", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
return
}

if message.reactionsArray().contains(where: { $0.reaction == reaction && $0.userReacted }) {
// We can't add reaction twice
return
Expand All @@ -2825,6 +2830,11 @@ import SwiftUI
}

func removeReaction(reaction: String, from message: NCChatMessage) {
if !self.room.canReact {
NotificationPresenter.shared().present(text: NSLocalizedString("You are not allowed to add or remove reactions in this conversation", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
return
}

self.setTemporaryReaction(reaction: reaction, withState: .removing, toMessage: message)

NCAPIController.sharedInstance().removeReaction(reaction, fromMessage: message.messageId, inRoom: self.room.token, for: self.account) { _, error, _ in
Expand Down
17 changes: 7 additions & 10 deletions NextcloudTalk/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ import SwiftUI

// Disable call buttons
self.callOptionsButton.isEnabled = false
} else if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room), !room.permissions.contains(.chat) {
} else if !room.canChat {
// Hide text input
self.setTextInputbarHidden(true, animated: isVisible)
} else if self.isTextInputbarHidden {
Expand Down Expand Up @@ -2311,9 +2311,7 @@ import SwiftUI
}

override func getContextMenuAccessoryView(forMessage message: NCChatMessage, forIndexPath indexPath: IndexPath, withCellHeight cellHeight: CGFloat) -> UIView? {
let hasChatPermissions = !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: room) || self.room.permissions.contains(.chat)

guard hasChatPermissions && self.isMessageReactable(message: message) else { return nil }
guard self.room.canReact && self.isMessageReactable(message: message) else { return nil }

let reactionViewPadding = 10
let emojiButtonPadding = 10
Expand Down Expand Up @@ -2432,7 +2430,6 @@ import SwiftUI

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

// Show edit information
if let lastEditActorDisplayName = message.lastEditActorDisplayName, message.lastEditTimestamp > 0 {
Expand All @@ -2457,14 +2454,14 @@ import SwiftUI
}

// Reply option
if self.isMessageReplyable(message: message), hasChatPermissions, !self.textInputbar.isEditing {
if self.isMessageReplyable(message: message), self.room.canChat, !self.textInputbar.isEditing {
actions.append(UIAction(title: NSLocalizedString("Reply", comment: ""), image: .init(systemName: "arrowshape.turn.up.left")) { _ in
self.didPressReply(for: message)
})
}

// Show "Add reaction" when running on MacOS because we don't have an accessory view
if self.isMessageReactable(message: message), hasChatPermissions, NCUtils.isiOSAppOnMac() {
if self.isMessageReactable(message: message), self.room.canReact, NCUtils.isiOSAppOnMac() {
actions.append(UIAction(title: NSLocalizedString("Add reaction", comment: ""), image: .init(systemName: "face.smiling")) { _ in
self.didPressAddReaction(for: message, at: indexPath)
})
Expand Down Expand Up @@ -2538,7 +2535,7 @@ import SwiftUI
}

// Re-send option
if (message.sendingFailed || message.isOfflineMessage) && hasChatPermissions {
if (message.sendingFailed || message.isOfflineMessage) && self.room.canChat {
actions.append(UIAction(title: NSLocalizedString("Resend", comment: ""), image: .init(systemName: "arrow.clockwise")) { _ in
self.didPressResend(for: message)
})
Expand Down Expand Up @@ -2609,14 +2606,14 @@ import SwiftUI
var destructiveMenuActions: [UIMenuElement] = []

// Edit option
if message.isEditable(for: self.account, in: self.room) && hasChatPermissions {
if message.isEditable(for: self.account, in: self.room) && self.room.canChat {
destructiveMenuActions.append(UIAction(title: NSLocalizedString("Edit", comment: "Edit a message or room participants"), image: .init(systemName: "pencil")) { _ in
self.didPressEdit(for: message)
})
}

// Delete option
if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: self.account, in: self.room) && hasChatPermissions) {
if message.sendingFailed || message.isOfflineMessage || (message.isDeletable(for: self.account, in: self.room) && self.room.canChat) {
destructiveMenuActions.append(UIAction(title: NSLocalizedString("Delete", comment: ""), image: .init(systemName: "trash"), attributes: .destructive) { _ in
self.didPressDelete(for: message)
})
Expand Down
1 change: 1 addition & 0 deletions NextcloudTalk/Database/NCDatabaseManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extern NSString * const kCapabilitySensitiveConversations;
extern NSString * const kCapabilityThreads;
extern NSString * const kCapabilityPinnedMessages;
extern NSString * const kCapabilityScheduleMessages;
extern NSString * const kCapabilityReactPermission;

extern NSString * const kNotificationsCapabilityExists;
extern NSString * const kNotificationsCapabilityTestPush;
Expand Down
1 change: 1 addition & 0 deletions NextcloudTalk/Database/NCDatabaseManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
NSString * const kCapabilityThreads = @"threads";
NSString * const kCapabilityPinnedMessages = @"pinned-messages";
NSString * const kCapabilityScheduleMessages = @"scheduled-messages";
NSString * const kCapabilityReactPermission = @"react-permission";

NSString * const kNotificationsCapabilityExists = @"exists";
NSString * const kNotificationsCapabilityTestPush = @"test-push";
Expand Down
1 change: 1 addition & 0 deletions NextcloudTalk/NCTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ typedef NS_OPTIONS(NSInteger, NCPermission) {
NCPermissionCanPublishVideo = 32,
NCPermissionCanPublishScreen = 64,
NCPermissionChat = 128,
NCPermissionReact = 256,
};

typedef NS_ENUM(NSInteger, NCMessageExpiration) {
Expand Down
14 changes: 14 additions & 0 deletions NextcloudTalk/Rooms/NCRoom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,5 +348,19 @@ import SwiftyAttributes
return self.permissions.contains(.canPublishScreen) || !supportsConversationPermissions
}

public var canChat: Bool {
// For very old servers without chat-permission capability, allow chat
return !NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityChatPermission, for: self) || self.permissions.contains(.chat)
}

public var canReact: Bool {
// Check if server supports separate react permission (Talk 24+)
if NCDatabaseManager.sharedInstance().roomHasTalkCapability(kCapabilityReactPermission, for: self) {
return self.permissions.contains(.react)
}

// Fallback for older servers: reactions were tied to chat permission
return self.canChat
}

}
3 changes: 3 additions & 0 deletions NextcloudTalk/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -2438,6 +2438,9 @@
/* No comment provided by engineer. */
"You are currently waiting in the lobby" = "You are currently waiting in the lobby";

/* No comment provided by engineer. */
"You are not allowed to add or remove reactions in this conversation" = "You are not allowed to add or remove reactions in this conversation";

/* No comment provided by engineer. */
"You are not part of any conversation" = "You are not part of any conversation";

Expand Down
39 changes: 39 additions & 0 deletions NextcloudTalkTests/UI/UIRoomTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@

textView.typeText(" Edited")

// TODO: Should change the lib to have a proper identifier here

Check warning on line 247 in NextcloudTalkTests/UI/UIRoomTest.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (Should change the lib to have ...) (todo)
// Save the edit
toolbar.buttons["selected"].tap()

Expand Down Expand Up @@ -284,4 +284,43 @@
let textView = toolbar.textViews["Write message, @ to mention someone …"]
XCTAssert(!textView.exists)
}

func testReactOnlyPermission() throws {
let app = launchAndLogin()

// ReactOnlyTest room is only created for Talk 24+ (main branch)
let reactOnlyCell = app.tables.cells.staticTexts["ReactOnlyTest"]

// Skip test if the room doesn't exist (older server versions)
try XCTSkipUnless(reactOnlyCell.waitForExistence(timeout: TestConstants.timeoutShort),
"ReactOnlyTest room not found - skipping (requires Talk 24+)")

reactOnlyCell.tap()

let chatNavBar = app.navigationBars["NextcloudTalk.ChatView"]
XCTAssert(chatNavBar.waitForExistence(timeout: TestConstants.timeoutLong))

// Find the message from alice that we should react to
let messageText = app.tables.textViews["React to this message!"].firstMatch
XCTAssert(messageText.waitForExistence(timeout: TestConstants.timeoutShort))

// Open context menu by long-pressing on the message
messageText.press(forDuration: 2.0)

// Tap the thumbs up reaction from the context menu (same pattern as testDeallocation)
let thumbsUpReaction = app.staticTexts["👍"]
XCTAssert(thumbsUpReaction.waitForExistence(timeout: TestConstants.timeoutShort))
thumbsUpReaction.tap()

// Verify the reaction was added - the reaction label shows "emoji count" format
let reactionLabel = app.staticTexts["👍 1"]
XCTAssert(reactionLabel.waitForExistence(timeout: TestConstants.timeoutShort),
"Reaction should be visible after adding it")

// Verify that we cannot send messages (no chat permission)
// The toolbar/text input should not be present when user cannot chat
let toolbar = app.toolbars["Toolbar"]
let textView = toolbar.textViews["Write message, @ to mention someone …"]
XCTAssertFalse(textView.exists, "Text input should not exist when user cannot chat")
}
}
4 changes: 1 addition & 3 deletions ShareExtension/ShareViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
return;
}

BOOL hasChatPermission = ![[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatPermission] || (room.permissions & NCPermissionChat) != 0;

if (!hasChatPermission || room.readOnlyState == NCRoomReadOnlyStateReadOnly) {
if (!room.canChat || room.readOnlyState == NCRoomReadOnlyStateReadOnly) {
[self showChatPermissionAlert];
return;
}
Expand Down
63 changes: 60 additions & 3 deletions ci-setup-rooms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@

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

SERVER_URL="http://localhost:8080"

# Check if a Talk capability is available
# Usage: has_capability "capability-name"
has_capability() {
local capability="$1"
curl -s -u admin:admin "$SERVER_URL/ocs/v2.php/cloud/capabilities" \
-H "OCS-APIRequest: true" \
-H 'accept: application/json, text/plain, */*' \
| jq -e ".ocs.data.capabilities.spreed.features | any(. == \"$capability\")" > /dev/null 2>&1
}

# Setup a room with lobby enabled and add admin as a normal participant
response=$(curl -u alice:alice 'http://localhost:8080/ocs/v2.php/apps/spreed/api/v4/room' \
response=$(curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room" \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/plain, */*' \
Expand All @@ -18,15 +30,60 @@ token=$(echo $response | jq -r .ocs.data.token)

echo $token

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

curl -u alice:alice "http://localhost:8080/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/plain, */*' \
--data-raw '{"newParticipant":"admin","source":"users"}'

# Setup a room with react-only permission (can react but cannot chat)
# Only available when server supports react-permission capability (Talk 24+)
if has_capability "react-permission"; then
echo "Setting up ReactOnlyTest room (react-permission capability detected)"

# Permission values: CustomPermissions=1 (auto-added), JoinCall=4, React=256
# Total: 260 (4 + 256, custom flag auto-added when non-zero)
response=$(curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room" \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/plain, */*' \
--data-raw '{"roomType":2,"roomName":"ReactOnlyTest"}')

echo $response

token=$(echo $response | jq -r .ocs.data.token)

echo "ReactOnlyTest token: $token"

# Set default permissions to react-only (no chat permission)
# 260 = JoinCall(4) + React(256)
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/permissions/default" \
-X 'PUT' \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/plain, */*' \
--data-raw '{"permissions":260}'

# Add admin as a participant (will get the default react-only permissions)
curl -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v4/room/$token/participants" \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/plain, */*' \
--data-raw '{"newParticipant":"admin","source":"users"}'

# Send a message from alice (moderator) so there's something to react to
curl -X POST -u alice:alice "$SERVER_URL/ocs/v2.php/apps/spreed/api/v1/chat/$token" \
-H "OCS-APIRequest: true" \
-H 'content-type: application/json' \
-H 'accept: application/json' \
--data-raw '{"message":"React to this message!"}'
else
echo "Skipping ReactOnlyTest room setup (react-permission capability not available)"
fi
Loading