diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt new file mode 100644 index 000000000..8b4ce5bc5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt @@ -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() +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.java deleted file mode 100644 index 1dc095e09..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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 { - - @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 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()); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt new file mode 100644 index 000000000..0c1feeba8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt @@ -0,0 +1,150 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.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(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? = 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java index dcc9e2370..a245c6045 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java @@ -74,6 +74,9 @@ public class TalkMessagesListAdapter extends MessagesListAda } else if (holder instanceof PreviewMessageViewHolder) { ((PreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatActivity); ((PreviewMessageViewHolder) holder).assignCommonMessageInterface(chatActivity); + + } else if (holder instanceof SystemMessageViewHolder) { + ((SystemMessageViewHolder) holder).assignSystemMessageInterface(chatActivity); } } } 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 45b9a1dc8..7a6952043 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -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, 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 = 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) { + 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) { - 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) { 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): List { + 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): Boolean { return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage .SystemMessageType.MESSAGE_DELETED 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 e38cffda1..0db7ea97b 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 @@ -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 { diff --git a/app/src/main/res/drawable/baseline_unfold_less_24.xml b/app/src/main/res/drawable/baseline_unfold_less_24.xml new file mode 100644 index 000000000..3956a5c30 --- /dev/null +++ b/app/src/main/res/drawable/baseline_unfold_less_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_unfold_more_24.xml b/app/src/main/res/drawable/baseline_unfold_more_24.xml new file mode 100644 index 000000000..3b01290af --- /dev/null +++ b/app/src/main/res/drawable/baseline_unfold_more_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/item_system_message.xml b/app/src/main/res/layout/item_system_message.xml index e9be2aa08..4448611b1 100644 --- a/app/src/main/res/layout/item_system_message.xml +++ b/app/src/main/res/layout/item_system_message.xml @@ -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"> - + - + + + android:orientation="vertical" + android:padding="@dimen/standard_half_padding" + app:alignContent="stretch" + app:alignItems="stretch" + app:flexWrap="wrap" + app:justifyContent="flex_end"> + + + + + + + + + + - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73992cc85..dca3644d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -376,6 +376,7 @@ How to translate with transifex: Add attachment Recent Backspace + See %1$s similar messages Guest access