Эх сурвалжийг харах

add ability to delete messages

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 4 жил өмнө
parent
commit
ded0cef839

+ 9 - 4
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

@@ -160,7 +160,11 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
 
 
         val resources = itemView.resources
         val resources = itemView.resources
 
 
-        val bg_bubble_color = resources.getColor(R.color.bg_message_list_incoming_bubble)
+        val bg_bubble_color = if (message.isDeleted) {
+            resources.getColor(R.color.bg_message_list_incoming_bubble_deleted)
+        } else {
+            resources.getColor(R.color.bg_message_list_incoming_bubble)
+        }
 
 
         var bubbleResource = R.drawable.shape_incoming_message
         var bubbleResource = R.drawable.shape_incoming_message
 
 
@@ -229,13 +233,14 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
 
 
         // parent message handling
         // parent message handling
 
 
-        message.parentMessage?.let { parentChatMessage ->
+        if (!message.isDeleted && message.parentMessage != null) {
+            var parentChatMessage = message.parentMessage
             parentChatMessage.activeUser = message.activeUser
             parentChatMessage.activeUser = message.activeUser
             quotedUserAvatar?.load(parentChatMessage.user.avatar) {
             quotedUserAvatar?.load(parentChatMessage.user.avatar) {
                 addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
                 addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
                 transformations(CircleCropTransformation())
                 transformations(CircleCropTransformation())
             }
             }
-            parentChatMessage.imageUrl?.let{
+            parentChatMessage.imageUrl?.let {
                 quotedMessagePreview?.visibility = View.VISIBLE
                 quotedMessagePreview?.visibility = View.VISIBLE
                 quotedMessagePreview?.load(it) {
                 quotedMessagePreview?.load(it) {
                     addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
                     addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
@@ -253,7 +258,7 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
             quotedMessageTime?.setTextColor(context!!.resources.getColor(R.color.warm_grey_four))
             quotedMessageTime?.setTextColor(context!!.resources.getColor(R.color.warm_grey_four))
             quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast)
             quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast)
             quotedChatMessageView?.visibility = View.VISIBLE
             quotedChatMessageView?.visibility = View.VISIBLE
-        } ?: run {
+        } else {
             quotedChatMessageView?.visibility = View.GONE
             quotedChatMessageView?.visibility = View.GONE
         }
         }
     }
     }

+ 13 - 7
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt

@@ -136,18 +136,23 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
             realView.isSelected = true
             realView.isSelected = true
         }
         }
         val resources = sharedApplication!!.resources
         val resources = sharedApplication!!.resources
+        val bg_bubble_color = if (message.isDeleted) {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted)
+        } else {
+            resources.getColor(R.color.bg_message_list_outcoming_bubble)
+        }
         if (message.isGrouped) {
         if (message.isGrouped) {
             val bubbleDrawable = getMessageSelector(
             val bubbleDrawable = getMessageSelector(
-                    resources.getColor(R.color.colorPrimary),
+                    bg_bubble_color,
                     resources.getColor(R.color.transparent),
                     resources.getColor(R.color.transparent),
-                    resources.getColor(R.color.colorPrimary),
+                    bg_bubble_color,
                     R.drawable.shape_grouped_outcoming_message)
                     R.drawable.shape_grouped_outcoming_message)
             ViewCompat.setBackground(bubble, bubbleDrawable)
             ViewCompat.setBackground(bubble, bubbleDrawable)
         } else {
         } else {
             val bubbleDrawable = getMessageSelector(
             val bubbleDrawable = getMessageSelector(
-                    resources.getColor(R.color.colorPrimary),
+                    bg_bubble_color,
                     resources.getColor(R.color.transparent),
                     resources.getColor(R.color.transparent),
-                    resources.getColor(R.color.colorPrimary),
+                    bg_bubble_color,
                     R.drawable.shape_outcoming_message)
                     R.drawable.shape_outcoming_message)
             ViewCompat.setBackground(bubble, bubbleDrawable)
             ViewCompat.setBackground(bubble, bubbleDrawable)
         }
         }
@@ -157,13 +162,14 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
 
 
         // parent message handling
         // parent message handling
 
 
-        message.parentMessage?.let { parentChatMessage ->
+        if (!message.isDeleted && message.parentMessage != null) {
+            var parentChatMessage = message.parentMessage
             parentChatMessage.activeUser = message.activeUser
             parentChatMessage.activeUser = message.activeUser
             quotedUserAvatar?.load(parentChatMessage.user.avatar) {
             quotedUserAvatar?.load(parentChatMessage.user.avatar) {
                 transformations(CircleCropTransformation())
                 transformations(CircleCropTransformation())
                 addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
                 addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
             }
             }
-            parentChatMessage.imageUrl?.let{
+            parentChatMessage.imageUrl?.let {
                 quotedMessagePreview?.visibility = View.VISIBLE
                 quotedMessagePreview?.visibility = View.VISIBLE
                 quotedMessagePreview?.load(it) {
                 quotedMessagePreview?.load(it) {
                     addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
                     addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
@@ -182,7 +188,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
             quoteColoredView?.setBackgroundResource(R.color.white)
             quoteColoredView?.setBackgroundResource(R.color.white)
 
 
             quotedChatMessageView?.visibility = View.VISIBLE
             quotedChatMessageView?.visibility = View.VISIBLE
-        } ?: run {
+        } else {
             quotedChatMessageView?.visibility = View.GONE
             quotedChatMessageView?.visibility = View.GONE
         }
         }
 
 

+ 7 - 1
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
 
 
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
 import com.nextcloud.talk.models.json.chat.ChatOverall;
 import com.nextcloud.talk.models.json.chat.ChatOverall;
+import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
 import com.nextcloud.talk.models.json.conversations.RoomsOverall;
 import com.nextcloud.talk.models.json.conversations.RoomsOverall;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
@@ -296,7 +297,8 @@ public interface NcApi {
 
 
     @FormUrlEncoded
     @FormUrlEncoded
     @POST
     @POST
-    Observable<GenericOverall> sendChatMessage(@Header("Authorization") String authorization, @Url String url,
+    Observable<GenericOverall> sendChatMessage(@Header("Authorization") String authorization,
+                                               @Url String url,
                                                @Field("message") CharSequence message,
                                                @Field("message") CharSequence message,
                                                @Field("actorDisplayName") String actorDisplayName,
                                                @Field("actorDisplayName") String actorDisplayName,
                                                @Field("replyTo") Integer replyTo);
                                                @Field("replyTo") Integer replyTo);
@@ -355,4 +357,8 @@ public interface NcApi {
     Observable<Response<GenericOverall>> uploadFile(@Header("Authorization") String authorization,
     Observable<Response<GenericOverall>> uploadFile(@Header("Authorization") String authorization,
                                                     @Url String url,
                                                     @Url String url,
                                                     @Body RequestBody body);
                                                     @Body RequestBody body);
+
+    @DELETE
+    Observable<ChatOverallSingleMessage> deleteChatMessage(@Header("Authorization") String authorization,
+                                                           @Url String url);
 }
 }

+ 98 - 3
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -78,6 +78,7 @@ import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
 import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatOverall
 import com.nextcloud.talk.models.json.chat.ChatOverall
+import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.chat.ReadStatus
 import com.nextcloud.talk.models.json.chat.ReadStatus
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomOverall
@@ -114,6 +115,7 @@ import org.greenrobot.eventbus.ThreadMode
 import org.parceler.Parcels
 import org.parceler.Parcels
 import retrofit2.HttpException
 import retrofit2.HttpException
 import retrofit2.Response
 import retrofit2.Response
+import java.net.HttpURLConnection
 import java.util.*
 import java.util.*
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import javax.inject.Inject
@@ -1049,7 +1051,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         if (!wasDetached) {
         if (!wasDetached) {
             if (lookIntoFuture > 0) {
             if (lookIntoFuture > 0) {
                 val finalTimeout = timeout
                 val finalTimeout = timeout
-                ncApi?.pullChatMessages(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap)
+                ncApi?.pullChatMessages(credentials,
+                        ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap)
                         ?.subscribeOn(Schedulers.io())
                         ?.subscribeOn(Schedulers.io())
                         ?.observeOn(AndroidSchedulers.mainThread())
                         ?.observeOn(AndroidSchedulers.mainThread())
                         ?.takeWhile { observable -> inConversation && !wasDetached }
                         ?.takeWhile { observable -> inConversation && !wasDetached }
@@ -1129,7 +1132,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         if (response.code() == 200) {
         if (response.code() == 200) {
 
 
             val chatOverall = response.body() as ChatOverall?
             val chatOverall = response.body() as ChatOverall?
-            val chatMessageList = chatOverall?.ocs!!.data
+            val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
 
 
             if (isFirstMessagesProcessing) {
             if (isFirstMessagesProcessing) {
                 cancelNotificationsForCurrentConversation()
                 cancelNotificationsForCurrentConversation()
@@ -1331,6 +1334,31 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         }
         }
     }
     }
 
 
+    private fun setDeletionFlagsAndRemoveInfomessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
+        val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
+        val chatMessageIterator = chatMessageMap.iterator()
+        while (chatMessageIterator.hasNext()) {
+            val currentMessage = chatMessageIterator.next()
+            if (isInfoMessageAboutDeletion(currentMessage)) {
+                if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
+                    // if chatMessageMap doesnt't contain message to delete (this happens when lookingIntoFuture),
+                    // the message to delete has to be modified directly inside the adapter
+                    setMessageAsDeleted(currentMessage.value.parentMessage)
+                } else {
+                    chatMessageMap[currentMessage.value.parentMessage.id]!!.isDeleted = true
+                }
+                chatMessageIterator.remove()
+            }
+        }
+        return chatMessageMap.values.toList()
+    }
+
+    private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
+        return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
+                .SystemMessageType.PARENT_MESSAGE_DELETED
+    }
+
+
     private fun startACall(isVoiceOnlyCall: Boolean) {
     private fun startACall(isVoiceOnlyCall: Boolean) {
         isLeavingForConversation = true
         isLeavingForConversation = true
         if (!isVoiceOnlyCall) {
         if (!isVoiceOnlyCall) {
@@ -1432,15 +1460,82 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
                         }
                         }
                         true
                         true
                     }
                     }
+                    R.id.action_delete_message -> {
+                        ncApi?.deleteChatMessage(
+                                credentials,
+                                ApiUtils.getUrlForMessageDeletion(conversationUser?.baseUrl, roomToken, message?.id)
+                        )?.subscribeOn(Schedulers.io())
+                                ?.observeOn(AndroidSchedulers.mainThread())
+                                ?.subscribe(object : Observer<ChatOverallSingleMessage> {
+                                    override fun onSubscribe(d: Disposable) {
+                                    }
+
+                                    override fun onNext(t: ChatOverallSingleMessage) {
+                                        if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
+                                            Toast.makeText(context, R.string.nc_delete_message_leaked_to_matterbridge,
+                                                    Toast.LENGTH_LONG).show()
+                                        }
+                                    }
+
+                                    override fun onError(e: Throwable) {
+                                        Log.e(TAG, "Something went wrong when trying to delete message with id " +
+                                                message?.id, e)
+                                        Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+                                    }
+
+                                    override fun onComplete() {
+                                    }
+                                })
+                        true
+                    }
                     else -> false
                     else -> false
                 }
                 }
             }
             }
             inflate(R.menu.chat_message_menu)
             inflate(R.menu.chat_message_menu)
+            menu.findItem(R.id.action_copy_message).isVisible = !(message as ChatMessage).isDeleted
             menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable
             menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable
-            show()
+            menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
+            if(menu.hasVisibleItems()){
+                show()
+            }
+        }
+    }
+
+    private fun setMessageAsDeleted(message: IMessage?) {
+        val messageTemp = message as ChatMessage
+        messageTemp.isDeleted = true
+
+        messageTemp.isOneToOneConversation = currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
+        messageTemp.isLinkPreviewAllowed = isLinkPreviewAllowed
+        messageTemp.activeUser = conversationUser
+
+        adapter?.update(messageTemp)
+    }
+
+    private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
+        if (conversationUser == null) return false
+
+        if(message.systemMessageType != ChatMessage.SystemMessageType.DUMMY) return false
+
+        if(message.isDeleted) return false
+
+        val sixHoursInMillis = 6 * 3600 * 1000
+        val isOlderThanSixHours = message.createdAt?.before(Date(System.currentTimeMillis() - sixHoursInMillis)) == true
+        if(isOlderThanSixHours) return false
+
+        val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
+            true
+        } else {
+            currentConversation!!.isParticipantOwnerOrModerator
         }
         }
+        if(!isUserAllowedByPrivileges) return false
+
+        if(!conversationUser.hasSpreedFeatureCapability("delete-messages")) return false
+
+        return true
     }
     }
 
 
+
     override fun hasContentFor(message: IMessage, type: Byte): Boolean {
     override fun hasContentFor(message: IMessage, type: Byte): Boolean {
         when (type) {
         when (type) {
             CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)
             CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)

+ 6 - 2
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java

@@ -21,6 +21,8 @@ package com.nextcloud.talk.models.json.chat;
 
 
 import android.text.TextUtils;
 import android.text.TextUtils;
 
 
+import androidx.annotation.Nullable;
+
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonIgnore;
 import com.bluelinelabs.logansquare.annotation.JsonIgnore;
 import com.bluelinelabs.logansquare.annotation.JsonObject;
 import com.bluelinelabs.logansquare.annotation.JsonObject;
@@ -42,7 +44,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
-import androidx.annotation.Nullable;
 import lombok.Data;
 import lombok.Data;
 
 
 @Parcel
 @Parcel
@@ -59,6 +60,8 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
     public Map<String, String> selectedIndividualHashMap;
     public Map<String, String> selectedIndividualHashMap;
     @JsonIgnore
     @JsonIgnore
     public boolean isLinkPreviewAllowed;
     public boolean isLinkPreviewAllowed;
+    @JsonIgnore
+    public boolean isDeleted;
     @JsonField(name = "id")
     @JsonField(name = "id")
     public int jsonMessageId;
     public int jsonMessageId;
     @JsonField(name = "token")
     @JsonField(name = "token")
@@ -283,6 +286,7 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
         FILE_SHARED,
         FILE_SHARED,
         LOBBY_NONE,
         LOBBY_NONE,
         LOBBY_NON_MODERATORS,
         LOBBY_NON_MODERATORS,
-        LOBBY_OPEN_TO_EVERYONE
+        LOBBY_OPEN_TO_EVERYONE,
+        PARENT_MESSAGE_DELETED
     }
     }
 }
 }

+ 36 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.java

@@ -0,0 +1,36 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.generic.GenericOCS;
+
+import org.parceler.Parcel;
+
+import lombok.Data;
+
+@Data
+@Parcel
+@JsonObject
+public class ChatOCSSingleMessage extends GenericOCS {
+    @JsonField(name = "data")
+    public ChatMessage data;
+}

+ 36 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.java

@@ -0,0 +1,36 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import org.parceler.Parcel;
+
+import lombok.Data;
+
+@Data
+@Parcel
+@JsonObject
+public class ChatOverallSingleMessage {
+    @JsonField(name = "ocs")
+    public ChatOCSSingleMessage ocs;
+}

+ 7 - 2
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java

@@ -110,8 +110,12 @@ public class Conversation {
     }
     }
 
 
     public boolean canModerate(UserEntity conversationUser) {
     public boolean canModerate(UserEntity conversationUser) {
-        return ((Participant.ParticipantType.OWNER.equals(participantType)
-                || Participant.ParticipantType.MODERATOR.equals(participantType)) && !isLockedOneToOne(conversationUser));
+        return (isParticipantOwnerOrModerator() && !isLockedOneToOne(conversationUser));
+    }
+
+    public boolean isParticipantOwnerOrModerator() {
+        return Participant.ParticipantType.OWNER.equals(participantType)
+                || Participant.ParticipantType.MODERATOR.equals(participantType);
     }
     }
 
 
     public boolean shouldShowLobby(UserEntity conversationUser) {
     public boolean shouldShowLobby(UserEntity conversationUser) {
@@ -121,6 +125,7 @@ public class Conversation {
     public boolean isLobbyViewApplicable(UserEntity conversationUser) {
     public boolean isLobbyViewApplicable(UserEntity conversationUser) {
         return !canModerate(conversationUser) && (getType() == ConversationType.ROOM_GROUP_CALL || getType() == ConversationType.ROOM_PUBLIC_CALL);
         return !canModerate(conversationUser) && (getType() == ConversationType.ROOM_GROUP_CALL || getType() == ConversationType.ROOM_PUBLIC_CALL);
     }
     }
+
     public boolean isNameEditable(UserEntity conversationUser) {
     public boolean isNameEditable(UserEntity conversationUser) {
         return (canModerate(conversationUser) && !ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL.equals(type));
         return (canModerate(conversationUser) && !ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL.equals(type));
     }
     }

+ 1 - 0
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt

@@ -63,6 +63,7 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             "lobby_none" -> return LOBBY_NONE
             "lobby_none" -> return LOBBY_NONE
             "lobby_non_moderators" -> return LOBBY_NON_MODERATORS
             "lobby_non_moderators" -> return LOBBY_NON_MODERATORS
             "lobby_timer_reached" -> return LOBBY_OPEN_TO_EVERYONE
             "lobby_timer_reached" -> return LOBBY_OPEN_TO_EVERYONE
+            "message_deleted" -> return PARENT_MESSAGE_DELETED
             else -> return DUMMY
             else -> return DUMMY
         }
         }
     }
     }

+ 4 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -292,4 +292,8 @@ public class ApiUtils {
     public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String filename) {
     public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String filename) {
         return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
         return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
     }
     }
+
+    public static String getUrlForMessageDeletion(String baseUrl, String token, String messageId) {
+        return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token + "/" + messageId;
+    }
 }
 }

+ 25 - 0
app/src/main/res/drawable/ic_delete_white_24dp.xml

@@ -0,0 +1,25 @@
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<vector android:autoMirrored="true" android:height="24dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FFFFFF" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
+</vector>

+ 8 - 3
app/src/main/res/menu/chat_message_menu.xml

@@ -6,12 +6,17 @@
         android:id="@+id/action_copy_message"
         android:id="@+id/action_copy_message"
         android:icon="@drawable/ic_content_copy_white_24dp"
         android:icon="@drawable/ic_content_copy_white_24dp"
         android:title="@string/nc_copy_message"
         android:title="@string/nc_copy_message"
-        app:showAsAction="always"/>
+        app:showAsAction="always" />
 
 
     <item
     <item
         android:id="@+id/action_reply_to_message"
         android:id="@+id/action_reply_to_message"
         android:icon="@drawable/ic_reply_white_24dp"
         android:icon="@drawable/ic_reply_white_24dp"
         android:title="@string/nc_reply"
         android:title="@string/nc_reply"
-        app:showAsAction="always"
-        />
+        app:showAsAction="always" />
+
+    <item
+        android:id="@+id/action_delete_message"
+        android:icon="@drawable/ic_delete_white_24dp"
+        android:title="@string/nc_delete_message"
+        app:showAsAction="always" />
 </menu>
 </menu>

+ 1 - 0
app/src/main/res/values-night/colors.xml

@@ -32,6 +32,7 @@
     <color name="nc_grey">@android:color/holo_purple</color>
     <color name="nc_grey">@android:color/holo_purple</color>
     <color name="bg_bottom_sheet">#222222</color>
     <color name="bg_bottom_sheet">#222222</color>
     <color name="bg_message_list_incoming_bubble">#484848</color>
     <color name="bg_message_list_incoming_bubble">#484848</color>
+    <color name="bg_message_list_incoming_bubble_deleted">#66484848</color>
 
 
     <color name="textColorMaxContrast">#8c8c8c</color>
     <color name="textColorMaxContrast">#8c8c8c</color>
 
 

+ 3 - 0
app/src/main/res/values/colors.xml

@@ -59,6 +59,9 @@
     <color name="bg_dark_mention_chips">#333333</color>
     <color name="bg_dark_mention_chips">#333333</color>
 
 
     <color name="bg_message_list_incoming_bubble">#EFEFEF</color>
     <color name="bg_message_list_incoming_bubble">#EFEFEF</color>
+    <color name="bg_message_list_incoming_bubble_deleted">#66EFEFEF</color>
+    <color name="bg_message_list_outcoming_bubble">@color/colorPrimary</color>
+    <color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
 
 
     <color name="bg_bottom_sheet">#46ffffff</color>
     <color name="bg_bottom_sheet">#46ffffff</color>
 </resources>
 </resources>

+ 10 - 4
app/src/main/res/values/strings.xml

@@ -19,6 +19,9 @@
   -->
   -->
 
 
 <resources xmlns:tools="http://schemas.android.com/tools">
 <resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Common -->
+    <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
+
     <!-- Bottom Navigation -->
     <!-- Bottom Navigation -->
     <string name="nc_settings">Settings</string>
     <string name="nc_settings">Settings</string>
 
 
@@ -320,6 +323,8 @@
     <string name="nc_99_plus">99+</string>
     <string name="nc_99_plus">99+</string>
     <string name="nc_copy_message">Copy</string>
     <string name="nc_copy_message">Copy</string>
     <string name="nc_reply">Reply</string>
     <string name="nc_reply">Reply</string>
+    <string name="nc_delete_message">Delete</string>
+    <string name="nc_delete_message_leaked_to_matterbridge">Message deleted successfully, but it might have been leaked to other services</string>
 
 
     <!-- Upload -->
     <!-- Upload -->
     <string name="nc_upload_local_file">Upload local file</string>
     <string name="nc_upload_local_file">Upload local file</string>
@@ -327,12 +332,13 @@
     <string name="nc_upload_failed">Failed to upload file</string>
     <string name="nc_upload_failed">Failed to upload file</string>
     <string name="nc_upload_choose_local_files">Choose files</string>
     <string name="nc_upload_choose_local_files">Choose files</string>
 
 
-    <!-- Non-translatable strings -->
-
-    <string name="path_password_strike_through" translatable="false"
-        tools:override="true">M3.27,4.27L19.74,20.74</string>
+    <!-- Phonebook Integration -->
     <string name="nc_settings_phone_book_integration_key" translatable="false">phone_book_integration</string>
     <string name="nc_settings_phone_book_integration_key" translatable="false">phone_book_integration</string>
     <string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut in phone book</string>
     <string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut in phone book</string>
     <string name="nc_settings_phone_book_integration_title">Phone book integration</string>
     <string name="nc_settings_phone_book_integration_title">Phone book integration</string>
     <string name="no_phone_book_integration_due_to_permissions">No phone book integration due to missing permissions</string>
     <string name="no_phone_book_integration_due_to_permissions">No phone book integration due to missing permissions</string>
+
+    <!-- Non-translatable strings -->
+    <string name="path_password_strike_through" translatable="false"
+        tools:override="true">M3.27,4.27L19.74,20.74</string>
 </resources>
 </resources>