diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt index 11301ffcf..e2f6b5c56 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ b/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 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 } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt index 6d59b4c1d..6560aef2b 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt @@ -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 } diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 66a56acd4..fbf23c522 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/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.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 sendChatMessage(@Header("Authorization") String authorization, @Url String url, + Observable 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> uploadFile(@Header("Authorization") String authorization, @Url String url, @Body RequestBody body); + + @DELETE + Observable deleteChatMessage(@Header("Authorization") String authorization, + @Url String url); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 6f2a929d7..2eea67577 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/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.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): List { + 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): 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 { + 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) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java index 4f8eec046..0bd9bd525 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java +++ b/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 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 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 } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.java new file mode 100644 index 000000000..408a9329b --- /dev/null +++ b/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 + * + * 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 . + */ +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; +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.java new file mode 100644 index 000000000..3918779aa --- /dev/null +++ b/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 + * + * 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 . + */ + +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; +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java index a63dd149f..ed990f941 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java @@ -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)); } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt index 48205eff3..cc791068c 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt @@ -63,6 +63,7 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter 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 } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index ba80f8f90..f7ade0a90 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/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) { 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; + } } diff --git a/app/src/main/res/drawable/ic_delete_white_24dp.xml b/app/src/main/res/drawable/ic_delete_white_24dp.xml new file mode 100644 index 000000000..4c3a52e84 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_white_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/menu/chat_message_menu.xml b/app/src/main/res/menu/chat_message_menu.xml index 9fb8c55fe..b5652a002 100644 --- a/app/src/main/res/menu/chat_message_menu.xml +++ b/app/src/main/res/menu/chat_message_menu.xml @@ -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" /> + app:showAsAction="always" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index e343fa4f7..35eb96282 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -32,6 +32,7 @@ @android:color/holo_purple #222222 #484848 + #66484848 #8c8c8c diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e93c0d118..ac56c22e8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -59,6 +59,9 @@ #333333 #EFEFEF + #66EFEFEF + @color/colorPrimary + #800082C9 #46ffffff diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af63da3d7..c92d68b95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,9 @@ --> + + Sorry, something went wrong! + Settings @@ -320,6 +323,8 @@ 99+ Copy Reply + Delete + Message deleted successfully, but it might have been leaked to other services Upload local file @@ -327,12 +332,13 @@ Failed to upload file Choose files - - - M3.27,4.27L19.74,20.74 + phone_book_integration Match contacts based on phone number to integrate Talk shortcut in phone book Phone book integration No phone book integration due to missing permissions + + + M3.27,4.27L19.74,20.74