group and collapse/expand system messages

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2023-08-25 14:25:52 +02:00
Родитель e838c5addf
Коммит e5794eb456
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: C793F8B59F43CE7B
10 изменённых файлов: 380 добавлений и 194 удалений

Просмотреть файл

@ -0,0 +1,8 @@
package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage
interface SystemMessageInterface {
fun expandSystemMessage(chatMessage: ChatMessage)
fun collapseSystemMessages()
}

Просмотреть файл

@ -1,112 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.view.View;
import android.view.ViewGroup;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.nextcloud.talk.utils.DateUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.stfalcon.chatkit.messages.MessageHolders;
import java.util.Map;
import javax.inject.Inject;
import androidx.core.view.ViewCompat;
import autodagger.AutoInjector;
@AutoInjector(NextcloudTalkApplication.class)
public class SystemMessageViewHolder extends MessageHolders.IncomingTextMessageViewHolder<ChatMessage> {
@Inject
AppPreferences appPreferences;
@Inject
Context context;
@Inject
DateUtils dateUtils;
protected ViewGroup background;
public SystemMessageViewHolder(View itemView) {
super(itemView);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
background = itemView.findViewById(R.id.container);
}
@Override
public void onBind(ChatMessage message) {
super.onBind(message);
Resources resources = itemView.getResources();
int pressedColor;
int mentionColor;
pressedColor = resources.getColor(R.color.bg_message_list_incoming_bubble);
mentionColor = resources.getColor(R.color.textColorMaxContrast);
Drawable bubbleDrawable = DisplayUtils.getMessageSelector(resources.getColor(R.color.transparent),
resources.getColor(R.color.transparent),
pressedColor,
R.drawable.shape_grouped_incoming_message);
ViewCompat.setBackground(background, bubbleDrawable);
Spannable messageString = new SpannableString(message.getText());
if (message.getMessageParameters() != null && message.getMessageParameters().size() > 0) {
for (String key : message.getMessageParameters().keySet()) {
Map<String, String> individualMap = message.getMessageParameters().get(key);
if (individualMap != null && individualMap.containsKey("name")) {
String searchText;
if ("user".equals(individualMap.get("type")) ||
"guest".equals(individualMap.get("type")) ||
"call".equals(individualMap.get("type"))
) {
searchText = "@" + individualMap.get("name");
} else {
searchText = individualMap.get("name");
}
messageString = DisplayUtils.searchAndColor(messageString, searchText, mentionColor);
}
}
}
text.setText(messageString);
if (time != null) {
time.setText(dateUtils.getLocalTimeStringFromTimestamp(message.getTimestamp()));
}
itemView.setTag(R.string.replyable_message_view_tag, message.getReplyable());
}
}

Просмотреть файл

@ -0,0 +1,150 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.annotation.SuppressLint
import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemSystemMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class SystemMessageViewHolder(itemView: View) : MessageHolders.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
private val binding: ItemSystemMessageBinding = ItemSystemMessageBinding.bind(itemView)
@JvmField
@Inject
var appPreferences: AppPreferences? = null
@JvmField
@Inject
var context: Context? = null
@JvmField
@Inject
var dateUtils: DateUtils? = null
protected var background: ViewGroup
lateinit var systemMessageInterface: SystemMessageInterface
init {
sharedApplication!!.componentApplication.inject(this)
background = itemView.findViewById(R.id.container)
}
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
val resources = itemView.resources
val pressedColor: Int = resources.getColor(R.color.bg_message_list_incoming_bubble)
val mentionColor: Int = resources.getColor(R.color.textColorMaxContrast)
val bubbleDrawable = DisplayUtils.getMessageSelector(
resources.getColor(R.color.transparent),
resources.getColor(R.color.transparent),
pressedColor,
R.drawable.shape_grouped_incoming_message
)
ViewCompat.setBackground(background, bubbleDrawable)
var messageString: Spannable = SpannableString(message.text)
if (message.messageParameters != null && message.messageParameters!!.size > 0) {
for (key in message.messageParameters!!.keys) {
val individualMap: Map<String?, String?>? = message.messageParameters!![key]
if (individualMap != null && individualMap.containsKey("name")) {
var searchText: String? = if ("user" == individualMap["type"] ||
"guest" == individualMap["type"] ||
"call" == individualMap["type"]
) {
"@" + individualMap["name"]
} else {
individualMap["name"]
}
messageString = DisplayUtils.searchAndColor(messageString, searchText, mentionColor)
}
}
}
binding.systemMessageLayout.visibility = View.VISIBLE
binding.similarMessagesHint.visibility = View.GONE
if (message.expandableParent) {
binding.expandCollapseIcon.visibility = View.VISIBLE
if (!message.isExpanded) {
val similarMessages = String.format(
sharedApplication!!.resources.getString(R.string.see_similar_system_messages),
message.expandableChildrenAmount
)
binding.messageText.text = messageString
binding.similarMessagesHint.visibility = View.VISIBLE
binding.similarMessagesHint.text = similarMessages
binding.expandCollapseIcon.setImageDrawable(
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_more_24)
)
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
binding.messageText.setOnClickListener { systemMessageInterface.expandSystemMessage(message) }
} else {
binding.messageText.text = messageString
binding.similarMessagesHint.visibility = View.GONE
binding.similarMessagesHint.text = ""
binding.expandCollapseIcon.setImageDrawable(
ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_less_24)
)
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
}
} else if (message.hiddenByCollapse) {
binding.systemMessageLayout.visibility = View.GONE
} else {
binding.expandCollapseIcon.visibility = View.GONE
binding.messageText.text = messageString
binding.expandCollapseIcon.setImageDrawable(null)
binding.systemMessageLayout.setOnClickListener(null)
}
if (!message.expandableParent && message.lastItemOfExpandableGroup != 0) {
binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() }
}
binding.messageTime.text = dateUtils!!.getLocalTimeStringFromTimestamp(message.timestamp)
itemView.setTag(R.string.replyable_message_view_tag, message.replyable)
}
fun assignSystemMessageInterface(systemMessageInterface: SystemMessageInterface) {
this.systemMessageInterface = systemMessageInterface
}
}

Просмотреть файл

@ -74,6 +74,9 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
} else if (holder instanceof PreviewMessageViewHolder) {
((PreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatActivity);
((PreviewMessageViewHolder) holder).assignCommonMessageInterface(chatActivity);
} else if (holder instanceof SystemMessageViewHolder) {
((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity);
}
}
}

Просмотреть файл

@ -137,6 +137,7 @@ import com.nextcloud.talk.adapters.messages.OutcomingTextMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.SystemMessageInterface
import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
@ -264,7 +265,8 @@ class ChatActivity :
ContentChecker<ChatMessage>,
VoiceMessageInterface,
CommonMessageInterface,
PreviewMessageInterface {
PreviewMessageInterface,
SystemMessageInterface {
var active = false
@ -1891,6 +1893,45 @@ class ChatActivity :
}
}
@SuppressLint("NotifyDataSetChanged")
override fun collapseSystemMessages() {
adapter?.items?.forEach {
if (it.item is ChatMessage) {
val chatMessage = it.item as ChatMessage
if (isChildOfExpandableSystemMessage(chatMessage)) {
chatMessage.hiddenByCollapse = true
}
chatMessage.isExpanded = false
}
}
adapter?.notifyDataSetChanged()
}
private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean {
return isSystemMessage(chatMessage) &&
!chatMessage.expandableParent &&
chatMessage.lastItemOfExpandableGroup != 0
}
@SuppressLint("NotifyDataSetChanged")
override fun expandSystemMessage(chatMessageToExpand: ChatMessage) {
adapter?.items?.forEach {
if (it.item is ChatMessage) {
val belongsToGroupToExpand =
(it.item as ChatMessage).lastItemOfExpandableGroup == chatMessageToExpand.lastItemOfExpandableGroup
if (belongsToGroupToExpand) {
(it.item as ChatMessage).hiddenByCollapse = false
}
}
}
chatMessageToExpand.isExpanded = true
adapter?.notifyDataSetChanged()
}
@SuppressLint("LongLogTag")
private fun downloadFileToCache(message: ChatMessage) {
message.isDownloadingVoiceMessage = true
@ -3085,7 +3126,14 @@ class ChatActivity :
Log.d(TAG, "pullChatMessages - HTTP_CODE_OK.")
val chatOverall = response.body() as ChatOverall?
val chatMessageList = handleSystemMessages(chatOverall?.ocs!!.data!!)
var chatMessageList = chatOverall?.ocs!!.data!!
chatMessageList = handleSystemMessages(chatMessageList)
determinePreviousMessageIds(chatMessageList)
handleExpandableSystemMessages(chatMessageList)
processHeaderChatLastGiven(response, lookIntoFuture)
@ -3100,6 +3148,8 @@ class ChatActivity :
processMessagesFromTheFuture(chatMessageList)
} else {
processMessagesNotFromTheFuture(chatMessageList)
collapseSystemMessages()
}
val newXChatLastCommonRead = response.headers()["X-Chat-Last-Common-Read"]?.let {
@ -3123,6 +3173,8 @@ class ChatActivity :
isFirstMessagesProcessing = false
binding.progressBar.visibility = View.GONE
binding.messagesListView.visibility = View.VISIBLE
collapseSystemMessages()
}
}
@ -3188,6 +3240,7 @@ class ChatActivity :
}
private fun processExpiredMessages() {
@SuppressLint("NotifyDataSetChanged")
fun deleteExpiredMessages() {
val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
@ -3248,8 +3301,6 @@ class ChatActivity :
adapter?.addToStart(unreadChatMessage, false)
}
determinePreviousMessageIds(chatMessageList)
addMessagesToAdapter(shouldAddNewMessagesNotice, chatMessageList)
if (shouldAddNewMessagesNotice && adapter != null) {
@ -3257,6 +3308,36 @@ class ChatActivity :
}
}
private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
var countGroupedMessages = 0
for (i in chatMessageList.indices) {
if (chatMessageList.size > i + 1) {
if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
) {
chatMessageList[i].isGrouped = true
countGroupedMessages++
} else {
countGroupedMessages = 0
}
}
val chatMessage = chatMessageList[i]
chatMessage.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
chatMessage.isFormerOneToOneConversation =
(currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE)
chatMessage.activeUser = conversationUser
}
if (adapter != null) {
adapter?.addToEnd(chatMessageList, false)
}
scrollToRequestedMessageIfNeeded()
}
private fun scrollToFirstUnreadMessage() {
adapter?.let {
layoutManager?.scrollToPositionWithOffset(
@ -3286,10 +3367,8 @@ class ChatActivity :
adapter?.let {
chatMessage.isGrouped = (
it.isPreviousSameAuthor(
chatMessage.actorId,
-1
) && it.getSameAuthorLastMessagesCount(chatMessage.actorId) %
it.isPreviousSameAuthor(chatMessage.actorId, -1) &&
it.getSameAuthorLastMessagesCount(chatMessage.actorId) %
GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
)
chatMessage.isOneToOneConversation =
@ -3318,37 +3397,6 @@ class ChatActivity :
}
}
private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
var countGroupedMessages = 0
determinePreviousMessageIds(chatMessageList)
for (i in chatMessageList.indices) {
if (chatMessageList.size > i + 1) {
if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
) {
chatMessageList[i].isGrouped = true
countGroupedMessages++
} else {
countGroupedMessages = 0
}
}
val chatMessage = chatMessageList[i]
chatMessage.isOneToOneConversation =
currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
chatMessage.isFormerOneToOneConversation =
(currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE)
chatMessage.activeUser = conversationUser
}
if (adapter != null) {
adapter?.addToEnd(chatMessageList, false)
}
scrollToRequestedMessageIfNeeded()
}
private fun determinePreviousMessageIds(chatMessageList: List<ChatMessage>) {
var previousMessageId = NO_PREVIOUS_MESSAGE_ID
for (i in chatMessageList.indices.reversed()) {
@ -3574,6 +3622,30 @@ class ChatActivity :
return chatMessageMap.values.toList()
}
private fun handleExpandableSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()
while (chatMessageIterator.hasNext()) {
val currentMessage = chatMessageIterator.next()
val previousMessage = chatMessageMap[currentMessage.value.previousMessageId.toString()]
if (isSystemMessage(currentMessage.value) &&
previousMessage?.systemMessageType == currentMessage.value.systemMessageType
) {
previousMessage?.expandableParent = true
currentMessage.value.expandableParent = false
if (currentMessage.value.lastItemOfExpandableGroup == 0) {
currentMessage.value.lastItemOfExpandableGroup = currentMessage.value.jsonMessageId
}
previousMessage?.lastItemOfExpandableGroup = currentMessage.value.lastItemOfExpandableGroup
previousMessage?.expandableChildrenAmount = currentMessage.value.expandableChildrenAmount + 1
}
}
return chatMessageMap.values.toList()
}
private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
.SystemMessageType.MESSAGE_DELETED

Просмотреть файл

@ -135,7 +135,17 @@ data class ChatMessage(
var voiceMessageSeekbarProgress: Int = 0,
var voiceMessageFloatArray: FloatArray? = null
var voiceMessageFloatArray: FloatArray? = null,
var expandableParent: Boolean = false,
var isExpanded: Boolean = false,
var lastItemOfExpandableGroup: Int = 0,
var expandableChildrenAmount: Int = 0,
var hiddenByCollapse: Boolean = false
) : Parcelable, MessageContentType, MessageContentType.Image {

Просмотреть файл

@ -0,0 +1,9 @@
<vector
android:height="24dp"
android:tint="?attr/colorControlNormal"
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="M7.41,18.59L8.83,20 12,16.83 15.17,20l1.41,-1.41L12,14l-4.59,4.59zM16.59,5.41L15.17,4 12,7.17 8.83,4 7.41,5.41 12,10l4.59,-4.59z"/>
</vector>

Просмотреть файл

@ -0,0 +1,9 @@
<vector
android:height="24dp"
android:tint="?attr/colorControlNormal"
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="M12,5.83L15.17,9l1.41,-1.41L12,3 7.41,7.59 8.83,9 12,5.83zM12,18.17L8.83,15l-1.41,1.41L12,21l4.59,-4.59L15.17,15 12,18.17z"/>
</vector>

Просмотреть файл

@ -24,52 +24,88 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_eighth_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_eighth_margin">
android:layout_height="wrap_content">
<com.google.android.flexbox.FlexboxLayout
android:id="@id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="vertical"
android:padding="@dimen/standard_half_padding"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/systemMessageLayout"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_eighth_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_eighth_margin">
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/messageText"
<ImageView
android:id="@+id/expandCollapseIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_margin="10dp"
android:contentDescription="@null"
android:visibility="gone"
tools:visibility="visible"/>
<com.google.android.flexbox.FlexboxLayout
android:id="@id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:gravity="center_horizontal"
android:textAlignment="center"
android:textColor="@color/textColorMaxContrast"
android:textSize="14sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
tools:text="System message" />
android:orientation="vertical"
android:padding="@dimen/standard_half_padding"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:gravity="center_horizontal"
android:textAlignment="center"
android:textColor="@color/textColorMaxContrast"
android:textSize="14sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
tools:text="System message" />
<TextView
android:id="@id/messageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:gravity="end"
android:textColor="@color/warm_grey_four"
android:textSize="12sp"
app:layout_alignSelf="center"
app:layout_flexGrow="1"
app:layout_wrapBefore="false"
tools:text="17:30" />
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/similarMessagesHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:gravity="center_horizontal"
android:textAlignment="center"
android:textColor="@color/grey_600"
android:textStyle="bold"
android:textSize="14sp"
app:layout_alignSelf="center"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
tools:text="See 5 similar messages" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>
<TextView
android:id="@id/messageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_half_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:gravity="end"
android:textColor="@color/warm_grey_four"
android:textSize="12sp"
app:layout_alignSelf="center"
app:layout_flexGrow="1"
app:layout_wrapBefore="false"
tools:text="17:30" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

Просмотреть файл

@ -376,6 +376,7 @@ How to translate with transifex:
<string name="nc_add_attachment">Add attachment</string>
<string name="emoji_category_recent">Recent</string>
<string name="emoji_backspace">Backspace</string>
<string name="see_similar_system_messages">See %1$s similar messages</string>
<!-- Conversation info guest access -->
<string name="nc_guest_access">Guest access</string>