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",