diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 949e97231fe..37675da7832 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -3975,6 +3975,10 @@ class ChatActivity : } override fun onClickReaction(chatMessage: ChatMessage, emoji: String) { + if (!participantPermissions.hasReactPermission()) { + Snackbar.make(binding.root, R.string.reaction_forbidden, Snackbar.LENGTH_LONG).show() + return + } VibrationUtils.vibrateShort(context) if (chatMessage.reactionsSelf?.contains(emoji) == true) { chatViewModel.deleteReaction(roomToken, chatMessage, emoji) @@ -3993,7 +3997,7 @@ class ChatActivity : roomToken, chatMessage, conversationUser, - participantPermissions.hasChatPermission(), + participantPermissions.hasReactPermission(), ncApi ).show() } @@ -4028,6 +4032,7 @@ class ChatActivity : currentConversation, isShowMessageDeletionButton(message), participantPermissions.hasChatPermission(), + participantPermissions.hasReactPermission(), spreedCapabilities ).show() } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 7f9c07c178a..6a03d67bdb3 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -69,6 +69,7 @@ class MessageActionsDialog( private val currentConversation: ConversationModel?, private val showMessageDeletionButton: Boolean, private val hasChatPermission: Boolean, + private val hasReactPermission: Boolean, private val spreedCapabilities: SpreedCapability ) : BottomSheetDialog(chatActivity) { @@ -138,7 +139,7 @@ class MessageActionsDialog( viewThemeUtils.material.colorBottomSheetBackground(dialogMessageActionsBinding.root) viewThemeUtils.material.colorBottomSheetDragHandle(dialogMessageActionsBinding.bottomSheetDragHandle) - initEmojiBar(hasChatPermission) + initEmojiBar(hasReactPermission) initMenuItemCopy(!message.isDeleted) initMenuItems(networkMonitor.isOnline.value) } @@ -264,9 +265,10 @@ class MessageActionsDialog( } } - private fun initEmojiBar(hasChatPermission: Boolean) { + private fun initEmojiBar(hasReactPermission: Boolean) { if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACTIONS) && - isPermitted(hasChatPermission) && + hasReactPermission && + isConversationWritable() && isReactableMessageType(message) ) { val recentEmojiManager = RecentEmojiManager(context, MAX_RECENTS) @@ -353,9 +355,8 @@ class MessageActionsDialog( } } - private fun isPermitted(hasChatPermission: Boolean): Boolean = - hasChatPermission && - ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY != + private fun isConversationWritable(): Boolean = + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY != currentConversation?.conversationReadOnlyState private fun isReactableMessageType(message: ChatMessage): Boolean = diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt index 4c9c727af90..bd2370a5318 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt @@ -47,7 +47,7 @@ class ShowReactionsDialog( private val roomToken: String, private val chatMessage: ChatMessage, private val user: User?, - private val hasChatPermission: Boolean, + private val hasReactPermission: Boolean, private val ncApi: NcApi ) : BottomSheetDialog(activity), ReactionItemClickListener { @@ -185,7 +185,7 @@ class ShowReactionsDialog( } override fun onClick(reactionItem: ReactionItem) { - if (hasChatPermission && reactionItem.reactionVoter.actorId?.equals(user?.userId) == true) { + if (hasReactPermission && reactionItem.reactionVoter.actorId?.equals(user?.userId) == true) { deleteReaction(chatMessage, reactionItem.reaction!!) adapter?.list?.remove(reactionItem) dismiss() diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index ac21acb5663..67b2e9a4a40 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -63,7 +63,8 @@ enum class SpreedFeatures(val value: String) { IMPORTANT_CONVERSATIONS("important-conversations"), THREADS("threads"), PINNED_MESSAGES("pinned-messages"), - SCHEDULED_MESSAGES("scheduled-messages") + SCHEDULED_MESSAGES("scheduled-messages"), + REACT_PERMISSION("react-permission") } @Suppress("TooManyFunctions") diff --git a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt index 4e57300f951..536d99281ed 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt @@ -26,6 +26,7 @@ class ParticipantPermissions( private val canPublishVideo = (conversation.permissions and PUBLISH_VIDEO) == PUBLISH_VIDEO val canPublishScreen = (conversation.permissions and PUBLISH_SCREEN) == PUBLISH_SCREEN private val hasChatPermission = (conversation.permissions and CHAT) == CHAT + private val hasReactPermission = (conversation.permissions and REACT) == REACT private fun hasConversationPermissions(): Boolean = CapabilitiesUtil.hasSpreedFeatureCapability( @@ -70,6 +71,15 @@ class ParticipantPermissions( return true } + fun hasReactPermission(): Boolean { + if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACT_PERMISSION)) { + // Server supports separate react permission - check REACT bit + return hasReactPermission + } + // Older server without react-permission capability - fall back to chat permission + return hasChatPermission() + } + companion object { val TAG = ParticipantPermissions::class.simpleName @@ -82,5 +92,6 @@ class ParticipantPermissions( const val PUBLISH_VIDEO = 32 const val PUBLISH_SCREEN = 64 const val CHAT = 128 + const val REACT = 256 } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8dd0ea77f50..7b7d68a75ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -550,6 +550,7 @@ How to translate with transifex: Sharing files from storage is not possible without permissions Open in Files app You are not allowed to share content to this chat + You are not allowed to add or remove reactions in this conversation is typing … are typing … diff --git a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt index 76b08b68a82..d66f51f4404 100644 --- a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt +++ b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt @@ -47,6 +47,101 @@ class ParticipantPermissionsTest : TestCase() { assertTrue(attendeePermissions.canPublishVideo()) } + @Test + fun test_reactPermissionWithReactCapability() { + val spreedCapability = SpreedCapability() + // Server with react-permission also supports chat-permission + spreedCapability.features = listOf("chat-permission", "react-permission") + val conversation = createConversation() + + // With react-permission capability, only REACT bit matters + conversation.permissions = ParticipantPermissions.REACT or + ParticipantPermissions.CUSTOM + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assertTrue(attendeePermissions.hasReactPermission()) + assertFalse(attendeePermissions.hasChatPermission()) + } + + @Test + fun test_reactPermissionDeniedWithReactCapability() { + val spreedCapability = SpreedCapability() + // Server with react-permission also supports chat-permission + spreedCapability.features = listOf("chat-permission", "react-permission") + val conversation = createConversation() + + // With react-permission capability, only CHAT but no REACT - should NOT allow reactions + conversation.permissions = ParticipantPermissions.CHAT or + ParticipantPermissions.CUSTOM + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assertFalse(attendeePermissions.hasReactPermission()) + assertTrue(attendeePermissions.hasChatPermission()) + } + + @Test + fun test_reactPermissionFallbackToChatOnOlderServer() { + val spreedCapability = SpreedCapability() + // Older server without react-permission capability but with chat-permission + spreedCapability.features = listOf("chat-permission") + val conversation = createConversation() + + // Only CHAT permission set - should allow reactions as fallback for older servers + conversation.permissions = ParticipantPermissions.CHAT or + ParticipantPermissions.CUSTOM + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assertTrue(attendeePermissions.hasReactPermission()) + assertTrue(attendeePermissions.hasChatPermission()) + } + + @Test + fun test_reactPermissionDeniedOnOlderServerWithoutChatPermission() { + val spreedCapability = SpreedCapability() + // Older server without react-permission capability but with chat-permission + spreedCapability.features = listOf("chat-permission") + val conversation = createConversation() + + // No CHAT permission - should deny reactions on older servers + conversation.permissions = ParticipantPermissions.CUSTOM + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assertFalse(attendeePermissions.hasReactPermission()) + assertFalse(attendeePermissions.hasChatPermission()) + } + private fun createConversation() = Conversation( token = "test",