Merge pull request #3608 from nextcloud/feature/edit_messages

Feature/edit messages
This commit is contained in:
Marcel Hibbe 2024-02-19 10:12:43 +01:00 committed by GitHub
commit e5f25bda67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 514 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:text="@string/nc_new_messages"
app:background="@color/colorPrimary"
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"
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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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