add ability to delete messages

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2021-01-21 22:37:20 +01:00
parent a71e3fcd54
commit ded0cef839
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
15 changed files with 264 additions and 26 deletions

View File

@ -160,7 +160,11 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
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
@ -229,13 +233,14 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
// parent message handling
message.parentMessage?.let { parentChatMessage ->
if (!message.isDeleted && message.parentMessage != null) {
var parentChatMessage = message.parentMessage
parentChatMessage.activeUser = message.activeUser
quotedUserAvatar?.load(parentChatMessage.user.avatar) {
addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
transformations(CircleCropTransformation())
}
parentChatMessage.imageUrl?.let{
parentChatMessage.imageUrl?.let {
quotedMessagePreview?.visibility = View.VISIBLE
quotedMessagePreview?.load(it) {
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))
quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast)
quotedChatMessageView?.visibility = View.VISIBLE
} ?: run {
} else {
quotedChatMessageView?.visibility = View.GONE
}
}

View File

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

View File

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

View File

@ -78,6 +78,7 @@ import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.chat.ChatMessage
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.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
@ -114,6 +115,7 @@ import org.greenrobot.eventbus.ThreadMode
import org.parceler.Parcels
import retrofit2.HttpException
import retrofit2.Response
import java.net.HttpURLConnection
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -1049,7 +1051,8 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
if (!wasDetached) {
if (lookIntoFuture > 0) {
val finalTimeout = timeout
ncApi?.pullChatMessages(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap)
ncApi?.pullChatMessages(credentials,
ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.takeWhile { observable -> inConversation && !wasDetached }
@ -1129,7 +1132,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
if (response.code() == 200) {
val chatOverall = response.body() as ChatOverall?
val chatMessageList = chatOverall?.ocs!!.data
val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
if (isFirstMessagesProcessing) {
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) {
isLeavingForConversation = true
if (!isVoiceOnlyCall) {
@ -1432,15 +1460,82 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
}
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
}
}
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
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 {
when (type) {
CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -110,8 +110,12 @@ public class Conversation {
}
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) {
@ -121,6 +125,7 @@ public class Conversation {
public boolean isLobbyViewApplicable(UserEntity conversationUser) {
return !canModerate(conversationUser) && (getType() == ConversationType.ROOM_GROUP_CALL || getType() == ConversationType.ROOM_PUBLIC_CALL);
}
public boolean isNameEditable(UserEntity conversationUser) {
return (canModerate(conversationUser) && !ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL.equals(type));
}

View File

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

View File

@ -292,4 +292,8 @@ public class ApiUtils {
public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String 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;
}
}

View File

@ -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>

View File

@ -6,12 +6,17 @@
android:id="@+id/action_copy_message"
android:icon="@drawable/ic_content_copy_white_24dp"
android:title="@string/nc_copy_message"
app:showAsAction="always"/>
app:showAsAction="always" />
<item
android:id="@+id/action_reply_to_message"
android:icon="@drawable/ic_reply_white_24dp"
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>

View File

@ -32,6 +32,7 @@
<color name="nc_grey">@android:color/holo_purple</color>
<color name="bg_bottom_sheet">#222222</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>

View File

@ -59,6 +59,9 @@
<color name="bg_dark_mention_chips">#333333</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>
</resources>

View File

@ -19,6 +19,9 @@
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Common -->
<string name="nc_common_error_sorry">Sorry, something went wrong!</string>
<!-- Bottom Navigation -->
<string name="nc_settings">Settings</string>
@ -320,6 +323,8 @@
<string name="nc_99_plus">99+</string>
<string name="nc_copy_message">Copy</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 -->
<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_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_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="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>