diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt index 17333d452..4533c8816 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt index 680af44a6..232b9e433 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt +++ b/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) { 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 674443ee6..493067de2 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -357,8 +357,8 @@ public interface NcApi { @FormUrlEncoded @PUT Observable> setPassword2(@Header("Authorization") String authorization, - @Url String url, - @Field("password") String password); + @Url String url, + @Field("password") String password); @GET Observable 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 editChatMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("message") String message); + @GET Observable> getSharedItems( @Header("Authorization") String authorization, @@ -463,8 +469,8 @@ public interface NcApi { @POST Observable setTypingStatusPrivacy(@Header("Authorization") String authorization, - @Url String url, - @Body RequestBody body); + @Url String url, + @Body RequestBody body); @POST Observable searchContactsByPhoneNumber(@Header("Authorization") String authorization, @@ -504,8 +510,8 @@ public interface NcApi { @Multipart @POST Observable uploadConversationAvatar(@Header("Authorization") String authorization, - @Url String url, - @Part MultipartBody.Part attachment); + @Url String url, + @Part MultipartBody.Part attachment); @GET Observable getEditableUserProfileFields(@Header("Authorization") String authorization, @@ -698,6 +704,6 @@ public interface NcApi { @FormUrlEncoded @PUT Observable setRecordingConsent(@Header("Authorization") String authorization, - @Url String url, - @Field("recordingConsent") int recordingConsent); -} + @Url String url, + @Field("recordingConsent") int recordingConsent); +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 81ffd18f4..d3cb4165d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/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(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 { + 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(R.id.micInputCloud)?.let { viewThemeUtils.talk.themeMicInputCloud(it) } + binding.messageInputView.findViewById(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): Boolean { + return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage + .SystemMessageType.MESSAGE_EDITED + } + private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry): 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,6 +4640,19 @@ class ChatActivity : } private fun isShowMessageDeletionButton(message: ChatMessage): Boolean { + val isUserAllowedByPrivileges = userAllowedByPrivilages(message) + + return when { + !isUserAllowedByPrivileges -> false + message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false + message.isDeleted -> false + !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false + !participantPermissions.hasChatPermission() -> false + else -> true + } + } + + fun userAllowedByPrivilages(message: ChatMessage): Boolean { if (conversationUser == null) return false val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) { @@ -4507,20 +4660,7 @@ class ChatActivity : } else { ConversationUtils.canModerate(currentConversation!!, conversationUser!!) } - - val isOlderThanSixHours = message - .createdAt - .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE)) - - return when { - !isUserAllowedByPrivileges -> false - isOlderThanSixHours -> false - message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false - message.isDeleted -> false - !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false - !participantPermissions.hasChatPermission() -> false - else -> true - } + return isUserAllowedByPrivileges } override fun hasContentFor(message: ChatMessage, type: Byte): Boolean { @@ -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 diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt index c9d70038a..467549314 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt +++ b/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, 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 9fe1a36b3..807e7a914 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 @@ -127,6 +127,7 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter 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 "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" diff --git a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt index 5da7243cc..18def2f05 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 16023ca15..ea6a9e936 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -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 } } diff --git a/app/src/main/res/drawable/ic_check_24.xml b/app/src/main/res/drawable/ic_check_24.xml new file mode 100644 index 000000000..fef2c90e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 000000000..70db409b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 000000000..1c9bd3e6b --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index f2b9361ca..b54e3de07 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -133,14 +133,15 @@ + + tools:visibility="visible" /> @@ -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"> - + tools:ignore="Overdraw" + tools:text="Marcel is typing"> @@ -259,6 +259,14 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_text_message.xml b/app/src/main/res/layout/item_custom_incoming_text_message.xml index fe6ac95ed..5a861f62c 100644 --- a/app/src/main/res/layout/item_custom_incoming_text_message.xml +++ b/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" /> + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml index 9298f5035..74457e9be 100644 --- a/app/src/main/res/layout/item_custom_outcoming_text_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml @@ -57,6 +57,7 @@ android:textIsSelectable="false" tools:text="Talk to you later!" /> + + + + + + + + + Caption Retrieval failed Languages could not be retrieved + Edit Message Icon + Edit + Send Edit Message + Clear Edit Message + Cannot Edit Messages older than 24 hours + Conversation is read Only + Edit Message Text + (edited) + Conversation not found Add to Notes + Edited by admin + "Edited by " + clear Edit Button + Edit Icon