Преглед на файлове

Merge pull request #3608 from nextcloud/feature/edit_messages

Feature/edit messages
Marcel Hibbe преди 1 година
родител
ревизия
e5f25bda67

+ 7 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt

@@ -79,7 +79,6 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
         sharedApplication!!.componentApplication.inject(this)
 
         setAvatarAndAuthorOnMessageItem(message)
-
         colorizeMessageBubble(message)
 
         itemView.isSelected = false
@@ -114,7 +113,13 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageText.text = processedMessageText
 
-        binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
+        if (message.lastEditTimestamp != 0L && !message.isDeleted) {
+            binding.messageEditIndicator.visibility = View.VISIBLE
+            binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp)
+        } else {
+            binding.messageEditIndicator.visibility = View.GONE
+            binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
+        }
 
         // parent message handling
         if (!message.isDeleted && message.parentMessage != null) {

+ 7 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt

@@ -104,7 +104,13 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
         viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT)
         binding.messageText.text = processedMessageText
 
-        binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
+        if (message.lastEditTimestamp != 0L && !message.isDeleted) {
+            binding.messageEditIndicator.visibility = View.VISIBLE
+            binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp)
+        } else {
+            binding.messageEditIndicator.visibility = View.GONE
+            binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
+        }
 
         // parent message handling
         if (!message.isDeleted && message.parentMessage != null) {

+ 15 - 9
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -357,8 +357,8 @@ public interface NcApi {
     @FormUrlEncoded
     @PUT
     Observable<Response<GenericOverall>> setPassword2(@Header("Authorization") String authorization,
-                                                     @Url String url,
-                                                     @Field("password") String password);
+                                                      @Url String url,
+                                                      @Field("password") String password);
 
     @GET
     Observable<CapabilitiesOverall> getCapabilities(@Header("Authorization") String authorization, @Url String url);
@@ -393,6 +393,12 @@ public interface NcApi {
                                                @Field("replyTo") Integer replyTo,
                                                @Field("silent") Boolean sendWithoutNotification);
 
+    @FormUrlEncoded
+    @PUT
+    Observable<ChatOverallSingleMessage> editChatMessage(@Header("Authorization") String authorization,
+                                                         @Url String url,
+                                                         @Field("message") String message);
+
     @GET
     Observable<Response<ChatShareOverall>> getSharedItems(
         @Header("Authorization") String authorization,
@@ -463,8 +469,8 @@ public interface NcApi {
 
     @POST
     Observable<GenericOverall> setTypingStatusPrivacy(@Header("Authorization") String authorization,
-                                                    @Url String url,
-                                                    @Body RequestBody body);
+                                                      @Url String url,
+                                                      @Body RequestBody body);
 
     @POST
     Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
@@ -504,8 +510,8 @@ public interface NcApi {
     @Multipart
     @POST
     Observable<RoomOverall> uploadConversationAvatar(@Header("Authorization") String authorization,
-                                            @Url String url,
-                                            @Part MultipartBody.Part attachment);
+                                                     @Url String url,
+                                                     @Part MultipartBody.Part attachment);
 
     @GET
     Observable<UserProfileFieldsOverall> getEditableUserProfileFields(@Header("Authorization") String authorization,
@@ -698,6 +704,6 @@ public interface NcApi {
     @FormUrlEncoded
     @PUT
     Observable<GenericOverall> setRecordingConsent(@Header("Authorization") String authorization,
-                                                    @Url String url,
-                                                    @Field("recordingConsent") int recordingConsent);
-}
+                                                   @Url String url,
+                                                   @Field("recordingConsent") int recordingConsent);
+}

+ 173 - 25
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -244,6 +244,7 @@ import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
+import io.reactivex.subjects.BehaviorSubject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -303,6 +304,8 @@ class ChatActivity :
 
     lateinit var chatViewModel: ChatViewModel
 
+    private lateinit var editMessage: ChatMessage
+
     override val view: View
         get() = binding.root
 
@@ -361,6 +364,8 @@ class ChatActivity :
         RELEASED,
         ERROR
     }
+    private val editableBehaviorSubject = BehaviorSubject.createDefault(false)
+    private val editedTextBehaviorSubject = BehaviorSubject.createDefault("")
 
     private var mediaRecorderState: MediaRecorderState = MediaRecorderState.INITIAL
 
@@ -763,7 +768,6 @@ class ChatActivity :
         })
 
         initMessageInputView()
-
         loadAvatarForStatusBar()
         setActionBarTitle()
 
@@ -774,10 +778,20 @@ class ChatActivity :
         val filters = arrayOfNulls<InputFilter>(1)
         val lengthFilter = CapabilitiesUtilNew.getMessageMaxLength(conversationUser)
 
+        binding.editView.editMessageView.visibility = View.GONE
+
+        if (editableBehaviorSubject.value!!) {
+            val editableText = Editable.Factory.getInstance().newEditable(editMessage.message)
+            binding.messageInputView.inputEditText.text = editableText
+            binding.messageInputView.inputEditText.setSelection(editableText.length)
+            binding.editView.editMessage.setText(editMessage.message)
+        }
+
         filters[0] = InputFilter.LengthFilter(lengthFilter)
         binding.messageInputView.inputEditText?.filters = filters
 
         binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
+
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
                 // unused atm
             }
@@ -796,6 +810,8 @@ class ChatActivity :
                 }
 
                 val editable = binding.messageInputView.inputEditText?.editableText
+                editedTextBehaviorSubject.onNext(editable.toString().trim())
+
                 if (editable != null && binding.messageInputView.inputEditText != null) {
                     val mentionSpans = editable.getSpans(
                         0,
@@ -827,20 +843,37 @@ class ChatActivity :
 
         // Image keyboard support
         // See: https://developer.android.com/guide/topics/text/image-keyboard
+
         (binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
             uploadFile(it.toString(), false)
         }
-
         initVoiceRecordButton()
+        if (editableBehaviorSubject.value!!) {
+            setEditUI()
+        }
 
         if (sharedText.isNotEmpty()) {
             binding.messageInputView.inputEditText?.setText(sharedText)
         }
+
         binding.messageInputView.setAttachmentsListener {
             AttachmentDialog(this, this).show()
         }
 
-        binding.messageInputView.button?.setOnClickListener { submitMessage(false) }
+        binding.messageInputView.button?.setOnClickListener {
+            submitMessage(false)
+        }
+
+        binding.messageInputView.editMessageButton.setOnClickListener {
+            if (editMessage.message == editedTextBehaviorSubject.value!!) {
+                clearEditUI()
+                return@setOnClickListener
+            }
+            editMessageAPI(editMessage, editedMessageText = editedTextBehaviorSubject.value!!)
+        }
+        binding.editView.clearEdit.setOnClickListener {
+            clearEditUI()
+        }
 
         if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-send")) {
             binding.messageInputView.button?.setOnLongClickListener {
@@ -853,6 +886,82 @@ class ChatActivity :
             resources?.getString(R.string.nc_description_send_message_button)
     }
 
+    private fun editMessageAPI(message: ChatMessage, editedMessageText: String) {
+        var apiVersion = 1
+        // FIXME Fix API checking with guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+        }
+
+        ncApi.editChatMessage(
+            credentials,
+            ApiUtils.getUrlForChatMessage(
+                apiVersion,
+                conversationUser?.baseUrl,
+                roomToken,
+                message?.id
+            ),
+            editedMessageText
+        )?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<ChatOverallSingleMessage> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(messageEdited: ChatOverallSingleMessage) {
+                    when (messageEdited.ocs?.meta?.statusCode) {
+                        HTTP_BAD_REQUEST -> {
+                            Snackbar.make(
+                                binding.root,
+                                getString(R.string.edit_error_24_hours_old_message),
+                                Snackbar.LENGTH_LONG
+                            ).show()
+                        }
+                        HTTP_FORBIDDEN -> {
+                            Snackbar.make(
+                                binding.root,
+                                getString(R.string.conversation_is_read_only),
+                                Snackbar.LENGTH_LONG
+                            ).show()
+                        }
+                        HTTP_NOT_FOUND -> {
+                            Snackbar.make(
+                                binding.root,
+                                "Conversation not found",
+                                Snackbar.LENGTH_LONG
+                            ).show()
+                        }
+                    }
+                    clearEditUI()
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "failed to edit message", e)
+                    Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
+                }
+
+                override fun onComplete() {
+                }
+            })
+    }
+
+    private fun setEditUI() {
+        binding.messageInputView.messageSendButton.visibility = View.GONE
+        binding.messageInputView.recordAudioButton.visibility = View.GONE
+        binding.messageInputView.editMessageButton.visibility = View.VISIBLE
+        binding.editView.editMessageView.visibility = View.VISIBLE
+        binding.messageInputView.attachmentButton.visibility = View.GONE
+    }
+
+    private fun clearEditUI() {
+        binding.messageInputView.editMessageButton.visibility = View.GONE
+        editableBehaviorSubject.onNext(false)
+        binding.messageInputView.inputEditText.setText("")
+        binding.editView.editMessageView.visibility = View.GONE
+        binding.messageInputView.attachmentButton.visibility = View.VISIBLE
+    }
+
     private fun themeMessageInputView() {
         binding.messageInputView.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) }
 
@@ -891,6 +1000,12 @@ class ChatActivity :
         binding.messageInputView.findViewById<MicInputCloud>(R.id.micInputCloud)?.let {
             viewThemeUtils.talk.themeMicInputCloud(it)
         }
+        binding.messageInputView.findViewById<ImageView>(R.id.editMessageButton)?.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+        binding.editView.clearEdit.let {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
     }
 
     private fun setupActionBar() {
@@ -1020,7 +1135,6 @@ class ChatActivity :
             R.layout.item_system_message,
             this
         )
-
         messageHolders.registerContentType(
             CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
             UnreadNoticeMessageViewHolder::class.java,
@@ -1079,10 +1193,12 @@ class ChatActivity :
     @SuppressLint("ClickableViewAccessibility")
     private fun initVoiceRecordButton() {
         if (!isVoiceRecordingLocked) {
-            if (binding.messageInputView.messageInput.text!!.isNotEmpty()) {
-                showMicrophoneButton(false)
-            } else {
-                showMicrophoneButton(true)
+            if (!editableBehaviorSubject.value!!) {
+                if (binding.messageInputView.messageInput.text!!.isNotEmpty()) {
+                    showMicrophoneButton(false)
+                } else {
+                    showMicrophoneButton(true)
+                }
             }
         } else if (mediaRecorderState == MediaRecorderState.RECORDING) {
             binding.messageInputView.playPauseBtn.visibility = View.GONE
@@ -1096,10 +1212,12 @@ class ChatActivity :
 
         isVoicePreviewPlaying = false
         binding.messageInputView.messageInput.doAfterTextChanged {
-            if (binding.messageInputView.messageInput.text?.isEmpty() == true) {
-                showMicrophoneButton(true)
-            } else {
-                showMicrophoneButton(false)
+            if (!editableBehaviorSubject.value!!) {
+                if (binding.messageInputView.messageInput.text?.isEmpty() == true) {
+                    showMicrophoneButton(true)
+                } else {
+                    showMicrophoneButton(false)
+                }
             }
         }
 
@@ -3851,6 +3969,12 @@ class ChatActivity :
                 chatMessageIterator.remove()
             } else if (isPollVotedMessage(currentMessage)) {
                 // delete poll system messages
+                chatMessageIterator.remove()
+            } else if (isEditMessage(currentMessage)) {
+                if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) {
+                    setMessageAsEdited(currentMessage.value.parentMessage)
+                }
+
                 chatMessageIterator.remove()
             }
         }
@@ -3892,6 +4016,11 @@ class ChatActivity :
             currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
     }
 
+    private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
+        return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
+            .SystemMessageType.MESSAGE_EDITED
+    }
+
     private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
         return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
     }
@@ -4453,6 +4582,17 @@ class ChatActivity :
         adapter?.update(messageTemp)
     }
 
+    private fun setMessageAsEdited(message: IMessage?) {
+        val messageTemp = message as ChatMessage
+        messageTemp.lastEditTimestamp = message.lastEditTimestamp
+
+        messageTemp.isOneToOneConversation =
+            currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
+        messageTemp.activeUser = conversationUser
+
+        adapter?.update(messageTemp)
+    }
+
     private fun updateAdapterForReaction(message: IMessage?) {
         val messageTemp = message as ChatMessage
 
@@ -4500,21 +4640,10 @@ class ChatActivity :
     }
 
     private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
-        if (conversationUser == null) return false
-
-        val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
-            true
-        } else {
-            ConversationUtils.canModerate(currentConversation!!, conversationUser!!)
-        }
-
-        val isOlderThanSixHours = message
-            .createdAt
-            .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
+        val isUserAllowedByPrivileges = userAllowedByPrivilages(message)
 
         return when {
             !isUserAllowedByPrivileges -> false
-            isOlderThanSixHours -> false
             message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
             message.isDeleted -> false
             !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
@@ -4523,6 +4652,17 @@ class ChatActivity :
         }
     }
 
+    fun userAllowedByPrivilages(message: ChatMessage): Boolean {
+        if (conversationUser == null) return false
+
+        val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
+            true
+        } else {
+            ConversationUtils.canModerate(currentConversation!!, conversationUser!!)
+        }
+        return isUserAllowedByPrivileges
+    }
+
     override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
         return when (type) {
             CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
@@ -4725,6 +4865,12 @@ class ChatActivity :
         startActivity(shareIntent)
     }
 
+    fun editMessage(message: ChatMessage) {
+        editableBehaviorSubject.onNext(true)
+        editMessage = message
+        initMessageInputView()
+    }
+
     companion object {
         private val TAG = ChatActivity::class.simpleName
         private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
@@ -4738,7 +4884,6 @@ class ChatActivity :
         private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
         private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000
         private const val HTTP_CODE_OK: Int = 200
-        private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
         private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
         private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
         private const val REQUEST_CODE_MESSAGE_SEARCH: Int = 777
@@ -4773,6 +4918,9 @@ class ChatActivity :
         private const val STATUS_SIZE_IN_DP = 9f
         private const val HTTP_CODE_NOT_MODIFIED = 304
         private const val HTTP_CODE_PRECONDITION_FAILED = 412
+        private const val HTTP_BAD_REQUEST = 400
+        private const val HTTP_FORBIDDEN = 403
+        private const val HTTP_NOT_FOUND = 404
         private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
         private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
         private const val MESSAGE_PULL_LIMIT = 100

+ 13 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt

@@ -121,6 +121,18 @@ data class ChatMessage(
     @JsonField(name = ["markdown"])
     var renderMarkdown: Boolean? = null,
 
+    @JsonField(name = ["lastEditActorDisplayName"])
+    var lastEditActorDisplayName: String? = null,
+
+    @JsonField(name = ["lastEditActorId"])
+    var lastEditActorId: String? = null,
+
+    @JsonField(name = ["lastEditActorType"])
+    var lastEditActorType: String? = null,
+
+    @JsonField(name = ["lastEditTimestamp"])
+    var lastEditTimestamp: Long = 0,
+
     var isDownloadingVoiceMessage: Boolean = false,
 
     var resetVoiceMessage: Boolean = false,
@@ -507,6 +519,7 @@ data class ChatMessage(
         GUEST_MODERATOR_PROMOTED,
         GUEST_MODERATOR_DEMOTED,
         MESSAGE_DELETED,
+        MESSAGE_EDITED,
         FILE_SHARED,
         OBJECT_SHARED,
         MATTERBRIDGE_CONFIG_ADDED,

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

@@ -127,6 +127,7 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             "guest_moderator_promoted" -> GUEST_MODERATOR_PROMOTED
             "guest_moderator_demoted" -> GUEST_MODERATOR_DEMOTED
             "message_deleted" -> MESSAGE_DELETED
+            "message_edited" -> ChatMessage.SystemMessageType.MESSAGE_EDITED
             "file_shared" -> FILE_SHARED
             "object_shared" -> OBJECT_SHARED
             "matterbridge_config_added" -> MATTERBRIDGE_CONFIG_ADDED
@@ -193,6 +194,7 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             GUEST_MODERATOR_PROMOTED -> "guest_moderator_promoted"
             GUEST_MODERATOR_DEMOTED -> "guest_moderator_demoted"
             MESSAGE_DELETED -> "message_deleted"
+            ChatMessage.SystemMessageType.MESSAGE_EDITED -> "message_edited"
             FILE_SHARED -> "file_shared"
             OBJECT_SHARED -> "object_shared"
             MATTERBRIDGE_CONFIG_ADDED -> "matterbridge_config_added"

+ 2 - 0
app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt

@@ -43,6 +43,7 @@ class MessageInput : MessageInput {
     lateinit var sendVoiceRecording: ImageView
     lateinit var micInputCloud: MicInputCloud
     lateinit var playPauseBtn: MaterialButton
+    lateinit var editMessageButton: ImageButton
     lateinit var seekBar: SeekBar
 
     constructor(context: Context?) : super(context) {
@@ -69,6 +70,7 @@ class MessageInput : MessageInput {
         micInputCloud = findViewById(R.id.micInputCloud)
         playPauseBtn = findViewById(R.id.playPauseBtn)
         seekBar = findViewById(R.id.seekbar)
+        editMessageButton = findViewById(R.id.editMessageButton)
     }
 
     var messageInput: EmojiEditText

+ 44 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -51,6 +51,8 @@ import com.nextcloud.talk.repositories.reactions.ReactionsRepository
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.ConversationUtils
+import com.nextcloud.talk.utils.DateConstants
+import com.nextcloud.talk.utils.DateUtils
 import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
 import com.vanniktech.emoji.EmojiPopup
 import com.vanniktech.emoji.EmojiTextView
@@ -60,6 +62,7 @@ import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
+import java.util.Date
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -78,6 +81,9 @@ class MessageActionsDialog(
     @Inject
     lateinit var reactionsRepository: ReactionsRepository
 
+    @Inject
+    lateinit var dateUtils: DateUtils
+
     private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
 
     private lateinit var popup: EmojiPopup
@@ -88,6 +94,17 @@ class MessageActionsDialog(
     private val messageHasRegularText = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message
         .getCalculateMessageType() && !message.isDeleted
 
+    private val isOlderThanTwentyFourHours = message
+        .createdAt
+        .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE))
+
+    private val isUserAllowedToEdit = chatActivity.userAllowedByPrivilages(message)
+
+    private val isMessageEditable = CapabilitiesUtilNew.hasSpreedFeatureCapability(
+        user,
+        "edit-messages"
+    ) && messageHasRegularText && !isOlderThanTwentyFourHours && isUserAllowedToEdit
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
@@ -129,6 +146,7 @@ class MessageActionsDialog(
                 ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
                 CapabilitiesUtilNew.isTranslationsSupported(user)
         )
+        initMenuEditorDetails(message.lastEditTimestamp != 0L && !message.isDeleted)
         initMenuReplyToMessage(message.replyable && hasChatPermission)
         initMenuReplyPrivately(
             message.replyable &&
@@ -136,6 +154,7 @@ class MessageActionsDialog(
                 hasUserActorId(message) &&
                 currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
         )
+        initMenuEditMessage(isMessageEditable)
         initMenuDeleteMessage(showMessageDeletionButton)
         initMenuForwardMessage(
             ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
@@ -317,10 +336,19 @@ class MessageActionsDialog(
                 dismiss()
             }
         }
-
         dialogMessageActionsBinding.menuDeleteMessage.visibility = getVisibility(visible)
     }
 
+    private fun initMenuEditMessage(visible: Boolean) {
+        dialogMessageActionsBinding.menuEditMessage.setOnClickListener {
+            chatActivity.editMessage(message)
+            Log.d("EDIT MESSAGE", "$message")
+            dismiss()
+        }
+
+        dialogMessageActionsBinding.menuEditMessage.visibility = getVisibility(visible)
+    }
+
     private fun initMenuReplyPrivately(visible: Boolean) {
         if (visible) {
             dialogMessageActionsBinding.menuReplyPrivately.setOnClickListener {
@@ -343,6 +371,20 @@ class MessageActionsDialog(
         dialogMessageActionsBinding.menuReplyToMessage.visibility = getVisibility(visible)
     }
 
+    private fun initMenuEditorDetails(showEditorDetails: Boolean) {
+        if (showEditorDetails) {
+            val editedTime = dateUtils.getLocalDateTimeStringFromTimestamp(
+                message.lastEditTimestamp *
+                    DateConstants.SECOND_DIVIDER
+            )
+
+            val editorName = context.getString(R.string.nc_edited_by) + message.lastEditActorDisplayName
+            dialogMessageActionsBinding.editorName.setText(editorName)
+            dialogMessageActionsBinding.editedTime.setText(editedTime)
+        }
+        dialogMessageActionsBinding.menuMessageEditedInfo.visibility = getVisibility(showEditorDetails)
+    }
+
     private fun initMenuItemCopy(visible: Boolean) {
         if (visible) {
             dialogMessageActionsBinding.menuCopyMessage.setOnClickListener {
@@ -485,5 +527,6 @@ class MessageActionsDialog(
         private const val ACTOR_LENGTH = 6
         private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
         private const val DELAY: Long = 200
+        private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000
     }
 }

+ 24 - 0
app/src/main/res/drawable/ic_check_24.xml

@@ -0,0 +1,24 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@color/fontAppbar" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>

+ 5 - 0
app/src/main/res/drawable/ic_clear_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>

+ 5 - 0
app/src/main/res/drawable/ic_edit_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
+</vector>

+ 20 - 12
app/src/main/res/layout/activity_chat.xml

@@ -133,14 +133,15 @@
             <include layout="@layout/item_custom_incoming_text_message_shimmer" />
 
             <include layout="@layout/item_custom_incoming_text_message_shimmer" />
+
         </LinearLayout>
 
         <com.stfalcon.chatkit.messages.MessagesList
             android:id="@+id/messagesListView"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:paddingBottom="20dp"
             android:clipToPadding="false"
+            android:paddingBottom="20dp"
             android:visibility="gone"
             app:dateHeaderTextSize="13sp"
             app:incomingBubblePaddingBottom="@dimen/message_bubble_corners_vertical_padding"
@@ -169,11 +170,10 @@
             app:outcomingTextSize="@dimen/chat_text_size"
             app:outcomingTimeTextSize="12sp"
             app:textAutoLink="all"
-            tools:visibility="visible"/>
+            tools:visibility="visible" />
 
         <com.nextcloud.ui.popupbubble.PopupBubble
             android:id="@+id/popupBubbleView"
-            android:theme="@style/Button.Primary"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_alignBottom="@id/typing_indicator_wrapper"
@@ -182,11 +182,12 @@
             android:layout_marginTop="16dp"
             android:layout_marginEnd="64dp"
             android:layout_marginBottom="26dp"
-            android:minHeight="@dimen/min_size_clickable_area"
             android:layout_toStartOf="@+id/scrollDownButton"
+            android:ellipsize="middle"
+            android:minHeight="@dimen/min_size_clickable_area"
             android:text="@string/nc_new_messages"
+            android:theme="@style/Button.Primary"
             app:background="@color/colorPrimary"
-            android:ellipsize="middle"
             app:cornerRadius="@dimen/button_corner_radius"
             app:icon="@drawable/ic_baseline_arrow_downward_24px" />
 
@@ -226,9 +227,9 @@
             android:id="@+id/typing_indicator_wrapper"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:orientation="vertical"
             android:layout_alignParentBottom="true"
-            android:layout_marginBottom="-19dp">
+            android:layout_marginBottom="-19dp"
+            android:orientation="vertical">
 
             <View
                 android:id="@+id/separator_1"
@@ -240,15 +241,14 @@
                 android:id="@+id/typing_indicator"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:maxLines="2"
-                android:ellipsize="end"
                 android:layout_marginStart="@dimen/side_margin"
                 android:layout_marginEnd="@dimen/side_margin"
                 android:background="@color/bg_default"
+                android:ellipsize="end"
+                android:maxLines="2"
                 android:textColor="@color/low_emphasis_text"
-                tools:text="Marcel is typing"
-                tools:ignore="Overdraw">
-            </TextView>
+                tools:ignore="Overdraw"
+                tools:text="Marcel is typing"></TextView>
 
         </LinearLayout>
 
@@ -259,6 +259,14 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
+        <include
+            android:id="@+id/editView"
+            layout="@layout/edit_message_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="6dp" >
+        </include>
+
         <com.nextcloud.talk.ui.MessageInput
             android:id="@+id/messageInputView"
             android:layout_width="match_parent"

+ 72 - 0
app/src/main/res/layout/dialog_message_actions.xml

@@ -127,6 +127,43 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
+        <LinearLayout
+            android:id="@+id/menu_message_edited_info"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="vertical"
+            tools:ignore="UseCompoundDrawables">
+
+            <TextView
+                android:id="@+id/editor_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="16dp"
+                android:paddingEnd="@dimen/standard_padding"
+                tools:text="@string/nc_edited_by_admin"
+                android:textAlignment="viewStart"
+                android:maxLines="1"
+                android:ellipsize="end"
+                android:textSize="15sp"
+                android:textColor = "@color/grey_600"/>
+
+            <TextView
+                android:id="@+id/edited_time"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="16dp"
+                android:paddingEnd="@dimen/standard_padding"
+                tools:text="12:30 AM"
+                android:textAlignment="viewStart"
+                android:textSize="15sp"
+                android:textColor ="@color/grey_600"/>
+
+        </LinearLayout>
+
         <LinearLayout
             android:id="@+id/menu_reply_to_message"
             android:layout_width="match_parent"
@@ -457,6 +494,41 @@
 
         </LinearLayout>
 
+
+
+        <LinearLayout
+            android:id="@+id/menu_edit_message"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
+
+            <ImageView
+                android:id="@+id/menu_icon_edit_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@string/edit_message_icon_description"
+                android:paddingStart="@dimen/standard_padding"
+                android:paddingEnd="@dimen/zero"
+                android:src="@drawable/ic_edit_24"
+                app:tint="@color/high_emphasis_menu_icon" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_edit_message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/standard_padding"
+                android:text="@string/nc_edit_message"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
         <LinearLayout
             android:id="@+id/menu_delete_message"
             android:layout_width="match_parent"

+ 60 - 0
app/src/main/res/layout/edit_message_view.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id = "@+id/editMessageView"
+    android:orientation = "horizontal">
+
+    <ImageView
+        android:id = "@+id/editIcon"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:padding = "12dp"
+        android:src = "@drawable/ic_edit_24"
+        android:layout_gravity = "start|top"
+        android:contentDescription= "@string/nc_edit_icon"
+        app:tint="@color/grey_600">
+
+    </ImageView>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight = "1"
+        android:orientation = "vertical">
+
+        <TextView
+            android:id = "@+id/editMessageTitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor = "@color/grey_600"
+            android:text = "@string/nc_edit_message_text">
+        </TextView>
+
+        <TextView
+            android:id = "@+id/editMessage"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines = "1"
+            tools:text = "Edit message very very very very very very very very very very long">
+        </TextView>
+
+    </LinearLayout>
+
+    <ImageView
+        android:id = "@+id/clearEdit"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:padding = "12dp"
+        android:src = "@drawable/ic_clear_24"
+        android:contentDescription="@string/nc_clear_edit_button"
+        android:gravity = "top|end">
+    </ImageView>
+
+</LinearLayout>

+ 18 - 2
app/src/main/res/layout/item_custom_incoming_text_message.xml

@@ -85,17 +85,33 @@
             android:layout_below="@id/messageText"
             android:layout_marginStart="8dp"
             android:alpha="0.6"
-            android:gravity="end"
             android:textColor="@color/no_emphasis_text"
             android:textIsSelectable="false"
+            android:gravity = "end"
             app:layout_alignSelf="center"
             app:layout_flexGrow="1"
             app:layout_wrapBefore="false"
             tools:text="12:38" />
 
+
+        <TextView
+            android:id="@+id/messageEditIndicator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            android:alpha="0.6"
+            android:textColor="@color/no_emphasis_text"
+            android:textIsSelectable="false"
+            app:layout_alignSelf="center"
+            android:text = "@string/hint_edited_message"
+            android:textSize="12sp">
+
+        </TextView>
+
         <include
             android:id="@+id/reactions"
             layout="@layout/reactions_inside_message" />
 
     </com.google.android.flexbox.FlexboxLayout>
-</RelativeLayout>
+</RelativeLayout>

+ 16 - 0
app/src/main/res/layout/item_custom_outcoming_text_message.xml

@@ -57,6 +57,7 @@
             android:textIsSelectable="false"
             tools:text="Talk to you later!" />
 
+
         <TextView
             android:id="@id/messageTime"
             android:layout_width="wrap_content"
@@ -72,6 +73,21 @@
             app:layout_wrapBefore="false"
             tools:text="10:35" />
 
+        <TextView
+            android:id="@+id/messageEditIndicator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            android:alpha="0.6"
+            android:textColor="@color/no_emphasis_text"
+            android:textIsSelectable="false"
+            app:layout_alignSelf="center"
+            android:text = "@string/hint_edited_message"
+            android:textSize="12sp">
+
+        </TextView>
+
         <ImageView
             android:id="@+id/checkMark"
             android:layout_width="wrap_content"

+ 15 - 1
app/src/main/res/layout/view_message_input.xml

@@ -71,7 +71,7 @@
             android:layout_height="wrap_content"
             android:layout_below="@+id/quotedChatMessageView"
             android:layout_centerHorizontal="true"
-            android:layout_toStartOf="@id/messageSendButton"
+            android:layout_marginEnd = "48dp"
             android:layout_toEndOf="@id/smileyButton"
             android:imeOptions="actionDone"
             android:inputType="textAutoCorrect|textMultiLine|textCapSentences"
@@ -229,6 +229,7 @@
             android:scaleType="centerInside"
             android:contentDescription="@string/nc_description_send_message_button" />
 
+
         <ImageButton
             android:id="@+id/recordAudioButton"
             android:layout_width="48dp"
@@ -239,6 +240,19 @@
             android:src="@drawable/ic_baseline_mic_24"
             android:contentDescription="@string/nc_description_record_voice" />
 
+
+        <ImageButton
+            android:id="@+id/editMessageButton"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_below="@id/quotedChatMessageView"
+            android:layout_alignParentEnd="true"
+            android:background="@color/transparent"
+            android:src="@drawable/ic_check_24"
+            android:visibility = "gone"
+            tools:visibility = "visible"
+            android:contentDescription="@string/nc_send_edit_message" />
+
         <Space
             android:id="@id/attachmentButtonSpace"
             android:layout_width="0dp"

+ 13 - 0
app/src/main/res/values/strings.xml

@@ -786,5 +786,18 @@ How to translate with transifex:
     <string name="nc_caption">Caption</string>
     <string name="languages_error_title">Retrieval failed</string>
     <string name="languages_error_message">Languages could not be retrieved</string>
+    <string name="edit_message_icon_description">Edit Message Icon</string>
+    <string name="nc_edit_message">Edit</string>
+    <string name="nc_send_edit_message">Send Edit Message</string>
+    <string name="nc_clear_edit_message">Clear Edit Message</string>
+    <string name="edit_error_24_hours_old_message">Cannot Edit Messages older than 24 hours</string>
+    <string name="conversation_is_read_only">Conversation is read Only</string>
+    <string name="nc_edit_message_text">Edit Message Text</string>
+    <string name="hint_edited_message">(edited)</string>
+    <string name="nc_conversation_not_found">Conversation not found</string>
     <string name="add_to_notes">Add to Notes</string>
+    <string name="nc_edited_by_admin">Edited by admin</string>
+    <string name="nc_edited_by">"Edited by "</string>
+    <string name="nc_clear_edit_button">clear Edit Button</string>
+    <string name="nc_edit_icon">Edit Icon</string>
 </resources>