From e305979cdd1ee18d1ef01c640d4e1d7f18931900 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Tue, 17 Mar 2020 18:34:43 +0100 Subject: [PATCH] New chat - baby steps Signed-off-by: Mario Danic --- app/build.gradle | 2 +- .../adapters/messages/ImageLoaderPayload.kt | 30 - .../MagicIncomingTextMessageViewHolder.kt | 231 --- .../MagicOutcomingTextMessageViewHolder.kt | 164 -- .../messages/MagicPreviewMessageViewHolder.kt | 216 --- .../messages/MagicSystemMessageViewHolder.kt | 81 - .../MagicUnreadNoticeMessageViewHolder.java | 52 - .../application/NextcloudTalkApplication.kt | 2 +- .../MentionAutocompleteCallback.java | 5 +- .../adapters/items/BrowserFileItem.kt | 3 +- .../controllers/BrowserController.kt | 7 +- .../filebrowser/operations/DavListing.java | 3 +- .../operations/ListingAbstractClass.java | 4 +- .../webdav/ReadFilesystemOperation.java | 3 +- .../talk/controllers/ChatController.kt | 1554 ----------------- .../controllers/ConversationInfoController.kt | 13 +- .../nextcloud/talk/jobs/NotificationWorker.kt | 2 +- .../models/json/conversations/Conversation.kt | 18 +- .../features/chat/ChatDateHeaderSource.kt | 56 + .../talk/newarch/features/chat/ChatElement.kt | 6 + .../newarch/features/chat/ChatElementTypes.kt | 11 + .../newarch/features/chat/ChatPresenter.kt | 216 +++ .../talk/newarch/features/chat/ChatView.kt | 296 ++-- .../newarch/features/chat/ChatViewModel.kt | 9 +- .../newarch/features/chat/ChatViewSource.kt | 12 + .../chat/interfaces/ImageLoaderInterface.kt | 9 + .../contactsflow/contacts/ContactsView.kt | 4 +- .../ConversationsListView.kt | 5 +- .../talk/newarch/local/models/User.kt | 11 + .../talk/newarch/local/models/UserNgEntity.kt | 8 - .../talk/newarch/services/CallService.kt | 4 +- .../nextcloud/talk/newarch/utils/Images.kt | 3 +- .../talk/utils/ConductorRemapping.kt | 6 +- .../com/nextcloud/talk/utils/DisplayUtils.kt | 5 +- .../nextcloud/talk/utils/bundle/BundleKeys.kt | 1 + app/src/main/res/layout/controller_chat.xml | 8 + .../item_custom_incoming_preview_message.xml | 89 - .../layout/rv_chat_incoming_preview_item.xml | 89 + ...age.xml => rv_chat_incoming_text_item.xml} | 6 +- ....xml => rv_chat_outgoing_preview_item.xml} | 6 +- ...age.xml => rv_chat_outgoing_text_item.xml} | 6 +- ...em_message.xml => rv_chat_system_item.xml} | 4 +- .../layout/rv_date_and_unread_notice_item.xml | 12 + app/src/main/res/values-cs-rCZ/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fi-rFI/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk-rSK/strings.xml | 2 +- app/src/main/res/values-sl/strings.xml | 2 +- app/src/main/res/values-sr/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 63 files changed, 650 insertions(+), 2662 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/ImageLoaderPayload.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicSystemMessageViewHolder.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicUnreadNoticeMessageViewHolder.java delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/chat/interfaces/ImageLoaderInterface.kt delete mode 100644 app/src/main/res/layout/item_custom_incoming_preview_message.xml create mode 100644 app/src/main/res/layout/rv_chat_incoming_preview_item.xml rename app/src/main/res/layout/{item_custom_incoming_text_message.xml => rv_chat_incoming_text_item.xml} (94%) rename app/src/main/res/layout/{item_custom_outcoming_preview_message.xml => rv_chat_outgoing_preview_item.xml} (94%) rename app/src/main/res/layout/{item_custom_outcoming_text_message.xml => rv_chat_outgoing_text_item.xml} (93%) rename app/src/main/res/layout/{item_system_message.xml => rv_chat_system_item.xml} (96%) create mode 100644 app/src/main/res/layout/rv_date_and_unread_notice_item.xml diff --git a/app/build.gradle b/app/build.gradle index 870460772..90c4cc81b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -155,7 +155,7 @@ android { ext { work_version = '2.3.3' - koin_version = "2.1.0-alpha-1" + koin_version = "2.1.4" lifecycle_version = '2.2.0' coil_version = "0.9.5" room_version = "2.2.4" diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/ImageLoaderPayload.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/ImageLoaderPayload.kt deleted file mode 100644 index 1c4eddc24..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/ImageLoaderPayload.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2019 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.os.Parcelable -import kotlinx.android.parcel.Parcelize -import kotlinx.android.parcel.RawValue - -@Parcelize -data class ImageLoaderPayload( - val map: @RawValue HashMap? -) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt deleted file mode 100644 index dcfe5f005..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ /dev/null @@ -1,231 +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.Intent -import android.graphics.drawable.Drawable -import android.graphics.drawable.LayerDrawable -import android.net.Uri -import android.text.Spannable -import android.text.SpannableString -import android.text.TextUtils -import android.util.TypedValue -import android.view.View -import android.widget.ImageView -import android.widget.RelativeLayout -import android.widget.TextView -import androidx.core.view.ViewCompat -import androidx.emoji.widget.EmojiTextView -import butterknife.BindView -import butterknife.ButterKnife -import com.amulyakhare.textdrawable.TextDrawable -import com.nextcloud.talk.R -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.utils.DisplayUtils -import com.nextcloud.talk.utils.TextMatchers -import com.nextcloud.talk.utils.preferences.AppPreferences -import com.stfalcon.chatkit.messages.MessageHolders -import org.koin.core.KoinComponent -import org.koin.core.inject - -class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders -.IncomingTextMessageViewHolder(incomingView), KoinComponent { - - @JvmField - @BindView(R.id.messageAuthor) - var messageAuthor: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageText) - var messageText: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageUserAvatar) - var messageUserAvatarView: ImageView? = null - - @JvmField - @BindView(R.id.messageTime) - var messageTimeView: TextView? = null - - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - - @JvmField - @BindView(R.id.quotedMessageAuthor) - var quotedUserName: EmojiTextView? = null - - @JvmField - @BindView(R.id.quotedMessageImage) - var quotedMessagePreview: ImageView? = null - - @JvmField - @BindView(R.id.quotedMessage) - var quotedMessage: EmojiTextView? = null - - @JvmField - @BindView(R.id.quoteColoredView) - var quoteColoredView: View? = null - - val context: Context by inject() - - val appPreferences: AppPreferences by inject() - - init { - ButterKnife.bind( - this, - itemView - ) - } - - override fun onBind(message: ChatMessage) { - super.onBind(message) - val author: String = message.actorDisplayName!! - if (!TextUtils.isEmpty(author)) { - messageAuthor!!.text = author - } else { - messageAuthor!!.setText(R.string.nc_nick_guest) - } - - if (!message.grouped && !message.oneToOneConversation) { - messageUserAvatarView!!.visibility = View.VISIBLE - if (message.actorType == "guests") { - // do nothing, avatar is set - } else if (message.actorType == "bots" && message.actorId == "changelog") { - val layers = arrayOfNulls(2) - layers[0] = context.getDrawable(R.drawable.ic_launcher_background) - layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground) - val layerDrawable = LayerDrawable(layers) - messageUserAvatarView?.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) - } else if (message.actorType == "bots") { - val drawable = TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRound( - ">", - context.resources.getColor(R.color.black) - ) - messageUserAvatarView!!.visibility = View.VISIBLE - messageUserAvatarView?.setImageDrawable(drawable) - } - } else { - if (message.oneToOneConversation) { - messageUserAvatarView!!.visibility = View.GONE - } else { - messageUserAvatarView!!.visibility = View.INVISIBLE - } - messageAuthor!!.visibility = View.GONE - } - - val resources = itemView.resources - - val bg_bubble_color = resources.getColor(R.color.bg_message_list_incoming_bubble) - - var bubbleResource = R.drawable.shape_incoming_message - - if (message.grouped) { - bubbleResource = R.drawable.shape_grouped_incoming_message - } - - val bubbleDrawable = DisplayUtils.getMessageSelector( - bg_bubble_color, - resources.getColor(R.color.transparent), - bg_bubble_color, bubbleResource - ) - ViewCompat.setBackground(bubble, bubbleDrawable) - - val messageParameters = message.messageParameters - - itemView.isSelected = false - messageTimeView!!.setTextColor(context.resources.getColor(R.color.warm_grey_four)) - - var messageString: Spannable = SpannableString(message.text) - - var textSize = context.resources.getDimension(R.dimen.chat_text_size) - - if (messageParameters != null && messageParameters.size > 0) { - for (key in messageParameters.keys) { - val individualHashMap = message.messageParameters!![key] - if (individualHashMap != null) { - if (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call") { - if (individualHashMap["id"] == message.activeUser!!.userId) { - messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser!!, - R.xml.chip_you - ) - } else { - messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser!!, - R.xml.chip_others - ) - } - } else if (individualHashMap["type"] == "file") { - itemView.setOnClickListener { v -> - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"])) - context.startActivity(browserIntent) - } - } - } - } - } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { - textSize = (textSize * 2.5).toFloat() - itemView.isSelected = true - messageAuthor!!.visibility = View.GONE - } - - messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - messageText!!.text = messageString - - // parent message handling - - message.parentMessage?.let { parentChatMessage -> - parentChatMessage.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - quotedMessagePreview?.visibility = View.VISIBLE - imageLoader.loadImage(quotedMessagePreview, it, null) - } ?: run { - quotedMessagePreview?.visibility = View.GONE - } - quotedUserName?.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - quotedMessage?.text = parentChatMessage.text - - quotedUserName?.setTextColor(context.resources.getColor(R.color.colorPrimary)) - - quoteColoredView?.setBackgroundResource(R.color.colorPrimary) - quotedChatMessageView?.visibility = View.VISIBLE - } ?: run { - quotedChatMessageView?.visibility = View.GONE - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt deleted file mode 100644 index a355a6ae3..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt +++ /dev/null @@ -1,164 +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.Intent -import android.net.Uri -import android.text.Spannable -import android.text.SpannableString -import android.util.TypedValue -import android.view.View -import android.widget.ImageView -import android.widget.RelativeLayout -import android.widget.TextView -import androidx.core.view.ViewCompat -import androidx.emoji.widget.EmojiTextView -import butterknife.BindView -import butterknife.ButterKnife -import com.google.android.flexbox.FlexboxLayout -import com.nextcloud.talk.R -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.utils.DisplayUtils.getMessageSelector -import com.nextcloud.talk.utils.DisplayUtils.searchAndReplaceWithMentionSpan -import com.nextcloud.talk.utils.TextMatchers -import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder -import org.koin.core.KoinComponent -import org.koin.core.inject -import java.util.* - -class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewHolder(itemView), KoinComponent { - @JvmField - @BindView(R.id.messageText) - var messageText: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageTime) - var messageTimeView: TextView? = null - - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - - @JvmField - @BindView(R.id.quotedMessageAuthor) - var quotedUserName: EmojiTextView? = null - - @JvmField - @BindView(R.id.quotedMessageImage) - var quotedMessagePreview: ImageView? = null - - @JvmField - @BindView(R.id.quotedMessage) - var quotedMessage: EmojiTextView? = null - - @JvmField - @BindView(R.id.quoteColoredView) - var quoteColoredView: View? = null - - val context: Context by inject() - private val realView: View - override fun onBind(message: ChatMessage) { - super.onBind(message) - val messageParameters: HashMap>? = message.messageParameters - var messageString: Spannable = SpannableString(message.text) - realView.isSelected = false - messageTimeView!!.setTextColor(context.resources.getColor(R.color.white60)) - val layoutParams = messageTimeView!!.layoutParams as FlexboxLayout.LayoutParams - layoutParams.isWrapBefore = false - var textSize = context.resources.getDimension(R.dimen.chat_text_size) - if (messageParameters != null && messageParameters.size > 0) { - for (key in messageParameters.keys) { - val individualHashMap: HashMap? = message.messageParameters!![key] - if (individualHashMap != null) { - if (individualHashMap["type"] == "user" || (individualHashMap["type"] - == "guest") || individualHashMap["type"] == "call") { - messageString = searchAndReplaceWithMentionSpan(messageText!!.context, - messageString, - individualHashMap["id"]!!, - individualHashMap["name"]!!, - individualHashMap["type"]!!, - message.activeUser!!, - R.xml.chip_others) - } else if (individualHashMap["type"] == "file") { - realView.setOnClickListener(View.OnClickListener { v: View? -> - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"])) - context.startActivity(browserIntent) - }) - } - } - } - } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { - textSize = (textSize * 2.5).toFloat() - layoutParams.isWrapBefore = true - messageTimeView!!.setTextColor(context.resources.getColor(R.color.warm_grey_four)) - realView.isSelected = true - } - val resources = sharedApplication!!.resources - if (message.grouped) { - val bubbleDrawable = getMessageSelector( - resources.getColor(R.color.bg_message_list_outcoming_bubble), - resources.getColor(R.color.transparent), - resources.getColor(R.color.bg_message_list_outcoming_bubble), - R.drawable.shape_grouped_outcoming_message) - ViewCompat.setBackground(bubble, bubbleDrawable) - } else { - val bubbleDrawable = getMessageSelector( - resources.getColor(R.color.bg_message_list_outcoming_bubble), - resources.getColor(R.color.transparent), - resources.getColor(R.color.bg_message_list_outcoming_bubble), - R.drawable.shape_outcoming_message) - ViewCompat.setBackground(bubble, bubbleDrawable) - } - messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - messageTimeView!!.layoutParams = layoutParams - messageText!!.text = messageString - - // parent message handling - - message.parentMessage?.let { parentChatMessage -> - parentChatMessage.activeUser = message.activeUser - parentChatMessage.imageUrl?.let { - quotedMessagePreview?.visibility = View.VISIBLE - imageLoader.loadImage(quotedMessagePreview, it, null) - } ?: run { - quotedMessagePreview?.visibility = View.GONE - } - quotedUserName?.text = parentChatMessage.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - quotedMessage?.text = parentChatMessage.text - quotedMessage?.setTextColor(context.resources.getColor(R.color.nc_outcoming_text_default)) - quotedUserName?.setTextColor(context.resources.getColor(R.color.nc_grey)) - - quoteColoredView?.setBackgroundResource(R.color.white) - - quotedChatMessageView?.visibility = View.VISIBLE - } ?: run { - quotedChatMessageView?.visibility = View.GONE - } - - } - - init { - ButterKnife.bind(this, itemView) - this.realView = itemView - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt deleted file mode 100644 index 07ce261a6..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt +++ /dev/null @@ -1,216 +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.annotation.SuppressLint -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.graphics.drawable.Drawable -import android.graphics.drawable.LayerDrawable -import android.net.Uri -import android.view.View -import androidx.emoji.widget.EmojiTextView -import butterknife.BindView -import butterknife.ButterKnife -import coil.api.load -import coil.transform.CircleCropTransformation -import com.nextcloud.talk.R.* -import com.nextcloud.talk.components.filebrowser.models.BrowserFile -import com.nextcloud.talk.components.filebrowser.models.DavResponse -import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.chat.ChatMessage.MessageType.* -import com.nextcloud.talk.newarch.local.models.UserNgEntity -import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp -import com.nextcloud.talk.utils.DisplayUtils.setClickableString -import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID -import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder -import io.reactivex.Single -import io.reactivex.SingleObserver -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import okhttp3.OkHttpClient -import org.koin.core.KoinComponent -import org.koin.core.inject - -class MagicPreviewMessageViewHolder(itemView: View?) : IncomingImageMessageViewHolder( - itemView -), KoinComponent { - @JvmField - @BindView(id.messageText) - var messageText: EmojiTextView? = null - val context: Context by inject() - val okHttpClient: OkHttpClient by inject() - - @SuppressLint("SetTextI18n") - override fun onBind(message: ChatMessage) { - super.onBind(message) - if (userAvatar != null) { - if (message.grouped || message.oneToOneConversation) { - if (message.oneToOneConversation) { - userAvatar.visibility = View.GONE - } else { - userAvatar.visibility = View.INVISIBLE - } - } else { - userAvatar.visibility = View.VISIBLE - if ("bots" == message.actorType && "changelog" == message.actorId) { - val layers = - arrayOfNulls(2) - layers[0] = context.getDrawable(drawable.ic_launcher_background) - layers[1] = context.getDrawable(drawable.ic_launcher_foreground) - val layerDrawable = - LayerDrawable(layers) - userAvatar.load(layerDrawable) { - transformations(CircleCropTransformation()) - } - } - } - } - - if (message.messageType == SINGLE_NC_ATTACHMENT_MESSAGE) { - // it's a preview for a Nextcloud share - - messageText!!.text = message.selectedIndividualHashMap!!["name"] - setClickableString( - message.selectedIndividualHashMap!!["name"]!!, - message.selectedIndividualHashMap!!["link"]!!, messageText!! - ) - - if (message.selectedIndividualHashMap!!.containsKey("mimetype")) { - if (message.imageUrl == "no-preview") { - image.load(getDrawableResourceIdForMimeType(message.selectedIndividualHashMap!!["mimetype"])) - } - } else { - fetchFileInformation( - "/" + message.selectedIndividualHashMap!!["path"], - message.activeUser - ) - } - - image.setOnClickListener { v: View? -> - val accountString = - message.activeUser!!.username + "@" + message.activeUser!! - .baseUrl - .replace("https://", "") - .replace("http://", "") - if (canWeOpenFilesApp(context, accountString)) { - val filesAppIntent = - Intent(Intent.ACTION_VIEW, null) - val componentName = ComponentName( - context.getString(string.nc_import_accounts_from), - "com.owncloud.android.ui.activity.FileDisplayActivity" - ) - filesAppIntent.component = componentName - filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - filesAppIntent.setPackage( - context.getString(string.nc_import_accounts_from) - ) - filesAppIntent.putExtra( - KEY_ACCOUNT, accountString - ) - filesAppIntent.putExtra( - KEY_FILE_ID, - message.selectedIndividualHashMap!!["id"] - ) - context.startActivity(filesAppIntent) - } else { - val browserIntent = Intent( - Intent.ACTION_VIEW, - Uri.parse(message.selectedIndividualHashMap!!["link"]) - ) - browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(browserIntent) - } - } - } else if (message.messageType == SINGLE_LINK_GIPHY_MESSAGE) { - messageText!!.text = "GIPHY" - setClickableString( - "GIPHY", "https://giphy.com", messageText!! - ) - } else if (message.messageType == SINGLE_LINK_TENOR_MESSAGE) { - messageText!!.text = "Tenor" - setClickableString( - "Tenor", "https://tenor.com", messageText!! - ) - } else { - if (message.messageType == SINGLE_LINK_IMAGE_MESSAGE) { - image.setOnClickListener { v: View? -> - val browserIntent = Intent( - Intent.ACTION_VIEW, Uri.parse(message.imageUrl) - ) - browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(browserIntent) - } - } else { - image.setOnClickListener(null) - } - messageText!!.text = "" - } - } - - private fun fetchFileInformation( - url: String, - activeUser: UserNgEntity? - ) { - Single.fromCallable { - ReadFilesystemOperation( - okHttpClient, activeUser, url, 0 - ) - } - .observeOn(Schedulers.io()) - .subscribe(object : SingleObserver { - override fun onSubscribe(d: Disposable) {} - override fun onSuccess(readFilesystemOperation: ReadFilesystemOperation) { - val davResponse: DavResponse = - readFilesystemOperation.readRemotePath() - if (davResponse.data != null) { - val browserFileList = - davResponse.data as List - if (browserFileList.isNotEmpty()) { - image.load(getDrawableResourceIdForMimeType(browserFileList[0].mimeType)) - } - } - } - - override fun onError(e: Throwable) {} - }) - } - - override fun getPayloadForImageLoader(message: ChatMessage): Any { - val map = HashMap() - // used for setting a placeholder - if (message.selectedIndividualHashMap!!.containsKey("mimetype")) { - map["mimetype"] = message.selectedIndividualHashMap!!["mimetype"]!! - } - - map["hasPreview"] = message.selectedIndividualHashMap!!.getOrDefault("has-preview", false) - - return ImageLoaderPayload(map) - } - - init { - ButterKnife.bind(this, itemView!!) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicSystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicSystemMessageViewHolder.kt deleted file mode 100644 index 3a5981147..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicSystemMessageViewHolder.kt +++ /dev/null @@ -1,81 +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.text.Spannable -import android.text.SpannableString -import android.view.View -import android.widget.TextView -import androidx.core.view.ViewCompat -import butterknife.BindView -import butterknife.ButterKnife -import com.nextcloud.talk.R -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.utils.DisplayUtils.getMessageSelector -import com.nextcloud.talk.utils.DisplayUtils.searchAndColor -import com.nextcloud.talk.utils.preferences.AppPreferences -import com.stfalcon.chatkit.messages.MessageHolders.IncomingTextMessageViewHolder -import com.stfalcon.chatkit.utils.DateFormatter -import org.koin.core.KoinComponent -import org.koin.core.inject -import java.util.* - -class MagicSystemMessageViewHolder(itemView: View) : IncomingTextMessageViewHolder(itemView), KoinComponent { - val appPreferences: AppPreferences by inject() - val context: Context by inject() - - @JvmField - @BindView(R.id.messageTime) - var messageTime: TextView? = null - - init { - ButterKnife.bind( - this, - itemView - ) - } - - override fun onBind(message: ChatMessage) { - super.onBind(message) - val resources = itemView.resources - val normalColor = resources.getColor(R.color.bg_message_list_incoming_bubble) - val pressedColor: Int - val mentionColor: Int - pressedColor = normalColor - mentionColor = resources.getColor(R.color.nc_author_text) - val bubbleDrawable = getMessageSelector(normalColor, - resources.getColor(R.color.transparent), pressedColor, - R.drawable.shape_grouped_incoming_message) - ViewCompat.setBackground(bubble, bubbleDrawable) - var messageString: Spannable = SpannableString(message.text) - if (message.messageParameters != null && message.messageParameters!!.size > 0) { - for (key in message.messageParameters!!.keys) { - val individualHashMap: HashMap? = message.messageParameters!![key] - if (individualHashMap != null && (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call")) { - messageString = searchAndColor(messageString, "@" + individualHashMap["name"], - mentionColor) - } - } - } - text.text = messageString - messageTime?.text = DateFormatter.format(message.createdAt, DateFormatter.Template.TIME) - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicUnreadNoticeMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicUnreadNoticeMessageViewHolder.java deleted file mode 100644 index 860664fe9..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicUnreadNoticeMessageViewHolder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2019 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.view.View; - -import com.nextcloud.talk.models.json.chat.ChatMessage; -import com.stfalcon.chatkit.messages.MessageHolders; - -public class MagicUnreadNoticeMessageViewHolder - extends MessageHolders.SystemMessageViewHolder { - - public MagicUnreadNoticeMessageViewHolder(View itemView) { - super(itemView); - } - - public MagicUnreadNoticeMessageViewHolder(View itemView, Object payload) { - super(itemView, payload); - } - - @Override - public void viewDetached() { - messagesListAdapter.deleteById("-1"); - } - - @Override - public void viewAttached() { - } - - @Override - public void viewRecycled() { - - } -} diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index ca1095e6d..6b1bf820e 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -176,7 +176,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration //endregion //region Protected methods - protected fun startKoin() { + private fun startKoin() { startKoin { androidContext(this@NextcloudTalkApplication) androidLogger() diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index 52744fba8..465f7adae 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -27,6 +27,7 @@ import android.widget.EditText; import com.nextcloud.talk.R; import com.nextcloud.talk.models.json.mention.Mention; +import com.nextcloud.talk.newarch.local.models.User; import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.BetterImageSpan; import com.nextcloud.talk.utils.DisplayUtils; @@ -38,10 +39,10 @@ import com.vanniktech.emoji.EmojiUtils; public class MentionAutocompleteCallback implements AutocompleteCallback { private Context context; - private UserNgEntity conversationUser; + private User conversationUser; private EditText editText; - public MentionAutocompleteCallback(Context context, UserNgEntity conversationUser, + public MentionAutocompleteCallback(Context context, User conversationUser, EditText editText) { this.context = context; this.conversationUser = conversationUser; diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt index 4caae9412..547639489 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt @@ -32,6 +32,7 @@ import coil.api.load import com.nextcloud.talk.R import com.nextcloud.talk.components.filebrowser.models.BrowserFile import com.nextcloud.talk.interfaces.SelectionInterface +import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.utils.ApiUtils @@ -47,7 +48,7 @@ import org.koin.core.inject class BrowserFileItem( val model: BrowserFile, - private val activeUser: UserNgEntity, + private val activeUser: User, private val selectionInterface: SelectionInterface ) : AbstractFlexibleItem(), IFilterable, KoinComponent { val context: Context by inject() diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt index b46c97772..5b8ba1722 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.kt @@ -40,6 +40,7 @@ import com.nextcloud.talk.components.filebrowser.operations.ListingAbstractClass import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.interfaces.SelectionInterface import com.nextcloud.talk.jobs.ShareOperationWorker +import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.bundle.BundleKeys import eu.davidea.fastscroller.FastScroller @@ -83,13 +84,13 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex private var listingAbstractClass: ListingAbstractClass? = null private val browserType: BrowserType private var currentPath: String? = null - private val activeUser: UserNgEntity + private val activeUser: User private val roomToken: String? init { setHasOptionsMenu(true) browserType = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_BROWSER_TYPE)) - activeUser = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_USER_ENTITY)) + activeUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY)!! roomToken = args.getString(BundleKeys.KEY_CONVERSATION_TOKEN) currentPath = "/" @@ -130,7 +131,7 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex iterator.remove() if (paths.size == 10 || !iterator.hasNext()) { data = Data.Builder() - .putLong(BundleKeys.KEY_INTERNAL_USER_ID, activeUser.id) + .putLong(BundleKeys.KEY_INTERNAL_USER_ID, activeUser.id!!) .putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken) .putStringArray(BundleKeys.KEY_FILE_PATHS, paths.toTypedArray()) .build() diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java index 8f054f787..d47550b90 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; import com.nextcloud.talk.components.filebrowser.models.DavResponse; import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation; +import com.nextcloud.talk.newarch.local.models.User; import com.nextcloud.talk.newarch.local.models.UserNgEntity; import java.util.concurrent.Callable; @@ -43,7 +44,7 @@ public class DavListing extends ListingAbstractClass { } @Override - public void getFiles(String path, UserNgEntity currentUser, @Nullable OkHttpClient okHttpClient) { + public void getFiles(String path, User currentUser, @Nullable OkHttpClient okHttpClient) { Single.fromCallable(new Callable() { @Override public ReadFilesystemOperation call() { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java index 8f941cac2..69f5348df 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java @@ -25,7 +25,7 @@ import android.os.Handler; import androidx.annotation.Nullable; import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; -import com.nextcloud.talk.newarch.local.models.UserNgEntity; +import com.nextcloud.talk.newarch.local.models.User; import okhttp3.OkHttpClient; @@ -38,7 +38,7 @@ public abstract class ListingAbstractClass { this.listingInterface = listingInterface; } - public abstract void getFiles(String path, UserNgEntity currentUser, + public abstract void getFiles(String path, User currentUser, @Nullable OkHttpClient okHttpClient); public void cancelAllJobs() { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java index 740e2b691..15cca9439 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java @@ -22,6 +22,7 @@ package com.nextcloud.talk.components.filebrowser.webdav; import com.nextcloud.talk.components.filebrowser.models.BrowserFile; import com.nextcloud.talk.components.filebrowser.models.DavResponse; +import com.nextcloud.talk.newarch.local.models.User; import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.newarch.utils.NetworkUtils; import com.nextcloud.talk.utils.ApiUtils; @@ -44,7 +45,7 @@ public class ReadFilesystemOperation { private final int depth; private final String basePath; - public ReadFilesystemOperation(OkHttpClient okHttpClient, UserNgEntity currentUser, String path, + public ReadFilesystemOperation(OkHttpClient okHttpClient, User currentUser, String path, int depth) { OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder(); okHttpClientBuilder.followRedirects(false); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt deleted file mode 100644 index a11177778..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ /dev/null @@ -1,1554 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2019 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.controllers - -import android.content.ClipData -import android.content.Context -import android.content.Intent -import android.content.res.Resources -import android.graphics.PorterDuff -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Parcelable -import android.text.Editable -import android.text.InputFilter -import android.text.TextUtils -import android.text.TextWatcher -import android.util.Log -import android.util.TypedValue -import android.view.* -import android.widget.* -import androidx.emoji.text.EmojiCompat -import androidx.emoji.widget.EmojiEditText -import androidx.emoji.widget.EmojiTextView -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import butterknife.BindView -import butterknife.OnClick -import coil.api.load -import coil.target.Target -import coil.transform.CircleCropTransformation -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler -import com.google.android.flexbox.FlexboxLayout -import com.nextcloud.talk.R -import com.nextcloud.talk.activities.MagicCallActivity -import com.nextcloud.talk.adapters.messages.* -import com.nextcloud.talk.api.NcApi -import com.nextcloud.talk.callbacks.MentionAutocompleteCallback -import com.nextcloud.talk.components.filebrowser.controllers.BrowserController -import com.nextcloud.talk.controllers.base.BaseController -import com.nextcloud.talk.events.UserMentionClickEvent -import com.nextcloud.talk.events.WebSocketCommunicationEvent -import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.chat.ChatOverall -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.ConversationOverall -import com.nextcloud.talk.models.json.conversations.RoomsOverall -import com.nextcloud.talk.models.json.generic.GenericOverall -import com.nextcloud.talk.models.json.mention.Mention -import com.nextcloud.talk.newarch.local.models.UserNgEntity -import com.nextcloud.talk.newarch.local.models.getCredentials -import com.nextcloud.talk.newarch.local.models.getMaxMessageLength -import com.nextcloud.talk.newarch.utils.Images -import com.nextcloud.talk.presenters.MentionAutocompletePresenter -import com.nextcloud.talk.utils.* -import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType -import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.text.Spans -import com.nextcloud.talk.webrtc.MagicWebSocketInstance -import com.nextcloud.talk.webrtc.WebSocketConnectionHelper -import com.otaliastudios.autocomplete.Autocomplete -import com.stfalcon.chatkit.commons.ImageLoader -import com.stfalcon.chatkit.commons.models.IMessage -import com.stfalcon.chatkit.messages.MessageHolders -import com.stfalcon.chatkit.messages.MessageInput -import com.stfalcon.chatkit.messages.MessagesList -import com.stfalcon.chatkit.messages.MessagesListAdapter -import com.stfalcon.chatkit.utils.DateFormatter -import com.uber.autodispose.AutoDispose -import com.vanniktech.emoji.EmojiPopup -import com.webianks.library.PopupBubble -import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import org.koin.android.ext.android.inject -import org.parceler.Parcels -import retrofit2.HttpException -import retrofit2.Response -import java.util.* -import java.util.concurrent.TimeUnit - -class ChatController(args: Bundle) : BaseController(), MessagesListAdapter -.OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter -.OnMessageViewLongClickListener, MessageHolders.ContentChecker { - - val ncApi: NcApi by inject() - - @BindView(R.id.messagesListView) - @JvmField - var messagesListView: MessagesList? = null - - @BindView(R.id.messageInputView) - @JvmField - var messageInputView: MessageInput? = null - - @BindView(R.id.messageInput) - @JvmField - var messageInput: EmojiEditText? = null - - @BindView(R.id.popupBubbleView) - @JvmField - var popupBubble: PopupBubble? = null - - @BindView(R.id.progressBar) - @JvmField - var loadingProgressBar: ProgressBar? = null - - @BindView(R.id.smileyButton) - @JvmField - var smileyButton: ImageButton? = null - - @BindView(R.id.lobbyView) - @JvmField - var lobbyView: RelativeLayout? = null - - @BindView(R.id.lobbyTextView) - @JvmField - var conversationLobbyText: TextView? = null - - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - var roomToken: String? = null - val conversationUser: UserNgEntity? - val roomPassword: String - var credentials: String? = null - var currentConversation: Conversation? = null - var inConversation = false - var historyRead = false - var globalLastKnownFutureMessageId: Long = -1 - var globalLastKnownPastMessageId: Long = -1 - var adapter: MessagesListAdapter? = null - var mentionAutocomplete: Autocomplete<*>? = null - var layoutManager: LinearLayoutManager? = null - var lookingIntoFuture = false - var newMessagesCount = 0 - var startCallFromNotification: Boolean? = null - val roomId: String - val voiceOnly: Boolean - var isFirstMessagesProcessing = true - var isLeavingForConversation: Boolean = false - var isLinkPreviewAllowed: Boolean = false - var wasDetached: Boolean = false - var emojiPopup: EmojiPopup? = null - - var myFirstMessage: CharSequence? = null - var checkingLobbyStatus: Boolean = false - - var conversationInfoMenuItem: MenuItem? = null - var conversationVoiceCallMenuItem: MenuItem? = null - var conversationVideoMenuItem: MenuItem? = null - - var magicWebSocketInstance: MagicWebSocketInstance? = null - - var lobbyTimerHandler: Handler? = null - val roomJoined: Boolean = false - - val imageLoader: coil.ImageLoader by inject() - - init { - setHasOptionsMenu(true) - - this.conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) - this.roomId = args.getString(BundleKeys.KEY_ROOM_ID, "") - this.roomToken = args.getString(BundleKeys.KEY_CONVERSATION_TOKEN, "") - - if (args.containsKey(BundleKeys.KEY_ACTIVE_CONVERSATION)) { - this.currentConversation = Parcels.unwrap( - args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION) - ) - } - - this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") - - if (conversationUser?.userId == "?") { - credentials = null - } else { - credentials = ApiUtils.getCredentials(conversationUser?.username, conversationUser?.token) - } - - if (args.containsKey(BundleKeys.KEY_OPEN_INCOMING_CALL)) { - this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_OPEN_INCOMING_CALL) - } - - this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false) - } - - private fun getRoomInfo() { - val shouldRepeat = conversationUser?.hasSpreedFeatureCapability("webinary-lobby") ?: false - if (shouldRepeat) { - checkingLobbyStatus = true - } - - - if (conversationUser != null) { - ncApi.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(conversationOverall: ConversationOverall) { - currentConversation = conversationOverall.ocs.data - loadAvatarForStatusBar() - - setTitle() - setupMentionAutocomplete() - checkReadOnlyState() - checkLobbyState() - - if (!inConversation) { - joinRoomWithPassword() - } - - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - if (shouldRepeat) { - if (lobbyTimerHandler == null) { - lobbyTimerHandler = Handler() - } - - lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) - } - } - }) - } - } - - private fun handleFromNotification() { - ncApi.getRooms(credentials, ApiUtils.getUrlForRoomEndpoint(conversationUser?.baseUrl)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(roomsOverall: RoomsOverall) { - for (conversation in roomsOverall.ocs.data) { - if (roomId == conversation.conversationId) { - roomToken = conversation.token - currentConversation = conversation - setTitle() - getRoomInfo() - break - } - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - } - - override fun inflateView( - inflater: LayoutInflater, - container: ViewGroup - ): View { - return inflater.inflate(R.layout.controller_chat, container, false) - } - - private fun loadAvatarForStatusBar() { - if (currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType - .ONE_TO_ONE_CONVERSATION && activity != null && conversationVoiceCallMenuItem != null - ) { - val avatarSize = DisplayUtils.convertDpToPixel( - conversationVoiceCallMenuItem?.icon!! - .intrinsicWidth.toFloat(), activity!! - ) - .toInt() - - avatarSize.let { - val target = object : Target { - override fun onSuccess(result: Drawable) { - super.onSuccess(result) - actionBar?.setIcon(result) - } - } - - // change lifecycle owner once we move to MVVM - val avatarRequest = Images().getRequestForUrl( - imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels( - conversationUser?.baseUrl, - currentConversation?.name, avatarSize / 2 - ), conversationUser, target, null, - CircleCropTransformation() - ) - - imageLoader.load(avatarRequest) - } - } - } - - override fun onViewBound(view: View) { - super.onViewBound(view) - - actionBar?.show() - var adapterWasNull = false - - if (adapter == null) { - loadingProgressBar?.visibility = View.VISIBLE - - adapterWasNull = true - - val messageHolders = MessageHolders() - messageHolders.setIncomingTextConfig( - MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message - ) - messageHolders.setOutcomingTextConfig( - MagicOutcomingTextMessageViewHolder::class.java, - R.layout.item_custom_outcoming_text_message - ) - - messageHolders.setIncomingImageConfig( - MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message - ) - messageHolders.setOutcomingImageConfig( - MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message - ) - - messageHolders.registerContentType( - CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, - this - ) - - messageHolders.registerContentType( - CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this - ) - - adapter = MessagesListAdapter( - conversationUser?.userId, messageHolders, ImageLoader { imageView, url, payload -> - - if (url != "no-preview") { - imageView.load(url) { - if (conversationUser != null && url!!.startsWith(conversationUser.baseUrl) && (url.contains( - "index.php/core/preview?fileId=") || url.contains("/avatar/"))) { - addHeader("Authorization", conversationUser.getCredentials()) - } - - if (url!!.contains("/avatar/")) { - transformations(CircleCropTransformation()) - } else { - if (payload is ImageLoaderPayload) { - payload.map?.get("mimetype")?.let { - val mimeTypeDrawableResource = getDrawableResourceIdForMimeType(it as String) - val drawable = context.getDrawable(mimeTypeDrawableResource) - placeholder(drawable) - error(drawable) - } - } - } - } - } - }) - } else { - messagesListView?.visibility = View.VISIBLE - } - - messagesListView?.setAdapter(adapter) - adapter?.setLoadMoreListener(this) - adapter?.setDateHeadersFormatter { format(it) } - adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } - - layoutManager = messagesListView?.layoutManager as LinearLayoutManager? - - popupBubble?.setRecyclerView(messagesListView) - - popupBubble?.setPopupBubbleListener { context -> - if (newMessagesCount != 0) { - val scrollPosition: Int - if (newMessagesCount - 1 < 0) { - scrollPosition = 0 - } else { - scrollPosition = newMessagesCount - 1 - } - Handler().postDelayed({ messagesListView?.smoothScrollToPosition(scrollPosition) }, 200) - } - } - - messagesListView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged( - recyclerView: RecyclerView, - newState: Int - ) { - super.onScrollStateChanged(recyclerView, newState) - - if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - if (newMessagesCount != 0 && layoutManager != null) { - if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < - newMessagesCount - ) { - newMessagesCount = 0 - - if (popupBubble != null && popupBubble!!.isShown) { - popupBubble?.hide() - } - } - } - } - } - }) - - val filters = arrayOfNulls(1) - val lengthFilter = conversationUser?.getMaxMessageLength() ?: 1000 - - filters[0] = InputFilter.LengthFilter(lengthFilter) - messageInput?.filters = filters - - messageInput?.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence, - start: Int, - count: Int, - after: Int - ) { - - } - - override fun onTextChanged( - s: CharSequence, - start: Int, - before: Int, - count: Int - ) { - if (s.length >= lengthFilter) { - messageInput?.error = String.format( - Objects.requireNonNull - (resources).getString(R.string.nc_limit_hit), Integer.toString(lengthFilter) - ) - } else { - messageInput?.error = null - } - - val editable = messageInput?.editableText - if (editable != null && messageInput != null) { - val mentionSpans = editable.getSpans( - 0, messageInput!!.length(), - Spans.MentionChipSpan::class.java - ) - var mentionSpan: Spans.MentionChipSpan - for (i in mentionSpans.indices) { - mentionSpan = mentionSpans[i] - if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd( - mentionSpan - ) - ) { - if (editable.subSequence( - editable.getSpanStart(mentionSpan), - editable.getSpanEnd(mentionSpan) - ).toString().trim { it <= ' ' } != mentionSpan.label - ) { - editable.removeSpan(mentionSpan) - } - } - } - } - } - - override fun afterTextChanged(s: Editable) { - - } - }) - - messageInputView?.setAttachmentsListener { - showBrowserScreen( - BrowserController - .BrowserType.DAV_BROWSER - ) - } - - messageInputView?.button?.setOnClickListener { v -> submitMessage() } - - messageInputView?.button?.contentDescription = resources?.getString( - R.string - .nc_description_send_message_button - ) - - if (currentConversation != null && currentConversation?.conversationId != null) { - loadAvatarForStatusBar() - checkLobbyState() - setTitle() - } - - if (adapterWasNull) { - // we're starting - if (TextUtils.isEmpty(roomToken)) { - handleFromNotification() - } else { - getRoomInfo() - } - } - } - - private fun checkReadOnlyState() { - if (currentConversation != null && conversationUser != null) { - if (currentConversation?.shouldShowLobby( - conversationUser - ) == true || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY - ) { - - conversationVoiceCallMenuItem?.icon?.alpha = 99 - conversationVideoMenuItem?.icon?.alpha = 99 - messageInputView?.visibility = View.GONE - - } else { - if (conversationVoiceCallMenuItem != null) { - conversationVoiceCallMenuItem?.icon?.alpha = 255 - } - - if (conversationVideoMenuItem != null) { - conversationVideoMenuItem?.icon?.alpha = 255 - } - - if (conversationUser != null && currentConversation != null && currentConversation!! - .shouldShowLobby(conversationUser) - ) { - messageInputView?.visibility = View.GONE - } else { - messageInputView?.visibility = View.VISIBLE - } - } - } - } - - private fun checkLobbyState() { - if (currentConversation != null && conversationUser != null && currentConversation?.isLobbyViewApplicable( - conversationUser - ) == true - ) { - - if (!checkingLobbyStatus) { - getRoomInfo() - } - - if (currentConversation?.shouldShowLobby(conversationUser) == true) { - lobbyView?.visibility = View.VISIBLE - messagesListView?.visibility = View.GONE - messageInputView?.visibility = View.GONE - loadingProgressBar?.visibility = View.GONE - - if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != - 0L - ) { - conversationLobbyText?.text = String.format( - resources!!.getString(R.string.nc_lobby_waiting_with_date), - DateUtils.getLocalDateStringFromTimestampForLobby( - currentConversation?.lobbyTimer - ?: 0 - ) - ) - } else { - conversationLobbyText?.setText(R.string.nc_lobby_waiting) - } - } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE - } - } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE - } - } - - private fun showBrowserScreen(browserType: BrowserController.BrowserType) { - val bundle = Bundle() - bundle.putParcelable( - BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType) - ) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) - bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken) - router.pushController( - RouterTransaction.with(BrowserController(bundle)) - .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler()) - ) - } - - private fun showConversationInfoScreen() { - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken) - router.pushController( - RouterTransaction.with(ConversationInfoController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()) - ) - } - - private fun setupMentionAutocomplete() { - activity?.let { - resources?.let { resources -> - val elevation = 6f - val backgroundDrawable = ColorDrawable(resources.getColor(R.color.bg_default)) - val presenter = MentionAutocompletePresenter(it, roomToken) - val callback = MentionAutocompleteCallback( - activity, - conversationUser, messageInput - ) - - if (mentionAutocomplete == null && messageInput != null) { - mentionAutocomplete = Autocomplete.on(messageInput) - .with(elevation) - .with(backgroundDrawable) - .with(MagicCharPolicy('@')) - .with(presenter) - .with(callback) - .build() - } - } - } - } - - override fun onAttach(view: View) { - super.onAttach(view) - setTitle() - eventBus.register(this) - - if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability( - "mention-flag" - ) == true && activity != null - ) { - activity?.findViewById(R.id.toolbar) - ?.setOnClickListener { v -> - showConversationInfoScreen() - } - } - - isLeavingForConversation = false - - isLinkPreviewAllowed = appPreferences.areLinkPreviewsAllowed - - emojiPopup = messageInput?.let { - EmojiPopup.Builder.fromRootView(view) - .setOnEmojiPopupShownListener { - if (resources != null) { - smileyButton?.setColorFilter( - resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN - ) - } - } - .setOnEmojiPopupDismissListener { - smileyButton?.setColorFilter( - resources!!.getColor(R.color.emoji_icons), - PorterDuff.Mode.SRC_IN - ) - } - .setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") } - .build(it) - } - - cancelNotificationsForCurrentConversation() - - if (inConversation) { - if (wasDetached && conversationUser?.hasSpreedFeatureCapability("no-ping") == true) { - currentConversation?.sessionId = "0" - wasDetached = false - joinRoomWithPassword() - } - } - } - - private fun cancelNotificationsForCurrentConversation() { - if (conversationUser != null) { - if (!conversationUser.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty(roomId)) { - NotificationUtils.cancelExistingNotificationsForRoom( - applicationContext, - conversationUser, roomId - ) - } else if (!TextUtils.isEmpty(roomToken)) { - NotificationUtils.cancelExistingNotificationsForRoom( - applicationContext, - conversationUser, roomToken!! - ) - } - } - } - - override fun onDetach(view: View) { - eventBus.unregister(this) - - if (activity != null) { - activity?.findViewById(R.id.toolbar) - ?.setOnClickListener(null) - } - - if (conversationUser != null && conversationUser.hasSpreedFeatureCapability("no-ping") - && activity != null && !activity?.isChangingConfigurations!! && !isLeavingForConversation - ) { - wasDetached = true - leaveRoom() - } - - if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { - mentionAutocomplete?.dismissPopup() - } - - super.onDetach(view) - } - - override fun getTitle(): String? { - if (currentConversation != null && currentConversation?.displayName != null) { - return currentConversation!!.displayName?.let { - EmojiCompat.get() - .process(it) - .toString() - } - } else { - return "" - } - } - - public override fun onDestroy() { - super.onDestroy() - - adapter = null - inConversation = false - } - - private fun startPing() { - if (conversationUser != null && !conversationUser.hasSpreedFeatureCapability("no-ping")) { - ncApi.pingCall( - credentials, ApiUtils.getUrlForCallPing( - conversationUser.baseUrl, - roomToken - ) - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } - ?.takeWhile { observable -> inConversation } - ?.retry(3) { observable -> inConversation } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(genericOverall: GenericOverall) { - - } - - override fun onError(e: Throwable) {} - - override fun onComplete() {} - }) - } - } - - @OnClick(R.id.smileyButton) - internal fun onSmileyClick() { - emojiPopup?.toggle() - } - - private fun joinRoomWithPassword() { - - if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) || - currentConversation?.sessionId == "0" - ) { - ncApi.joinRoom( - credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), - roomPassword - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(3) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(conversationOverall: ConversationOverall) { - inConversation = true - currentConversation?.sessionId = conversationOverall.ocs.data.sessionId - startPing() - - setupWebsocket() - checkLobbyState() - - if (isFirstMessagesProcessing) { - pullChatMessages(0) - } else { - pullChatMessages(1) - } - - if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken!!, - currentConversation?.sessionId - ) - } - if (startCallFromNotification != null && startCallFromNotification == true) { - startCallFromNotification = false - startACall(voiceOnly) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - } else { - inConversation = true - if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( - roomToken!!, - currentConversation?.sessionId - ) - } - startPing() - if (isFirstMessagesProcessing) { - pullChatMessages(0) - } else { - pullChatMessages(1) - } - } - } - - private fun leaveRoom() { - ncApi.leaveRoom( - credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant( - conversationUser?.baseUrl, - roomToken - ) - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(genericOverall: GenericOverall) { - checkingLobbyStatus = false - - if (lobbyTimerHandler != null) { - lobbyTimerHandler?.removeCallbacksAndMessages(null) - } - - if (magicWebSocketInstance != null && currentConversation != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( - "", - currentConversation?.sessionId - ) - } - - if (!isDestroyed && !isBeingDestroyed && !wasDetached) { - router.popCurrentController() - } - } - - override fun onError(e: Throwable) {} - - override fun onComplete() { - } - }) - } - - private fun setSenderId() { - try { - val senderId = adapter?.javaClass?.getDeclaredField("senderId") - senderId?.isAccessible = true - senderId?.set(adapter, conversationUser?.userId) - } catch (e: NoSuchFieldException) { - Log.w(TAG, "Failed to set sender id") - } catch (e: IllegalAccessException) { - Log.w(TAG, "Failed to access and set field") - } - - } - - private fun submitMessage() { - if (messageInput != null) { - val editable = messageInput!!.editableText - val mentionSpans = editable.getSpans( - 0, editable.length, - Spans.MentionChipSpan::class.java - ) - var mentionSpan: Spans.MentionChipSpan - for (i in mentionSpans.indices) { - mentionSpan = mentionSpans[i] - var mentionId = mentionSpan.id - if (mentionId.contains(" ") || mentionId.startsWith("guest/")) { - mentionId = "\"" + mentionId + "\"" - } - editable.replace( - editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId" - ) - } - - messageInput?.setText("") - val replyMessageId: Long? = view?.findViewById(R.id.quotedChatMessageView)?.tag as Long? - sendMessage(editable, if (view?.findViewById(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) replyMessageId?.toInt() else null) - cancelReply() - } - } - - private fun sendMessage(message: CharSequence, replyTo: Int?) { - - if (conversationUser != null) { - ncApi.sendChatMessage( - credentials, ApiUtils.getUrlForChat( - conversationUser.baseUrl, - roomToken - ), - message, conversationUser.displayName, replyTo - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - - } - - override fun onNext(genericOverall: GenericOverall) { - myFirstMessage = message - - if (popupBubble?.isShown == true) { - popupBubble?.hide() - } - - messagesListView?.smoothScrollToPosition(0) - } - - override fun onError(e: Throwable) { - if (e is HttpException) { - val code = e.code() - if (Integer.toString(code).startsWith("2")) { - myFirstMessage = message - - if (popupBubble?.isShown == true) { - popupBubble?.hide() - } - - messagesListView?.smoothScrollToPosition(0) - } - } - } - - override fun onComplete() { - - } - }) - } - } - - private fun setupWebsocket() { - if (conversationUser != null) { - if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId( - conversationUser.id - ) != null - ) { - magicWebSocketInstance = - WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) - } else { - magicWebSocketInstance = null - } - } - } - - private fun pullChatMessages(lookIntoFuture: Int) { - if (!inConversation) { - return - } - - if (currentConversation != null && conversationUser != null && currentConversation!! - .shouldShowLobby(conversationUser) - ) { - return - } - - val fieldMap = HashMap() - fieldMap["includeLastKnown"] = 0 - - var timeout = 30 - if (!lookingIntoFuture) { - timeout = 0 - } - - fieldMap["timeout"] = timeout - - if (lookIntoFuture > 0) { - lookingIntoFuture = true - } else if (isFirstMessagesProcessing) { - if (currentConversation != null) { - globalLastKnownFutureMessageId = currentConversation!!.lastReadMessageId - globalLastKnownPastMessageId = currentConversation!!.lastReadMessageId - fieldMap["includeLastKnown"] = 1 - } - } - - fieldMap["lookIntoFuture"] = lookIntoFuture - fieldMap["limit"] = 100 - fieldMap["setReadMarker"] = 1 - - val lastKnown: Long - if (lookIntoFuture > 0) { - lastKnown = globalLastKnownFutureMessageId - } else { - lastKnown = globalLastKnownPastMessageId - } - - fieldMap["lastKnownMessageId"] = lastKnown.toInt() - - if (!wasDetached) { - if (lookIntoFuture > 0) { - val finalTimeout = timeout - ncApi.pullChatMessages( - credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(response: Response<*>) { - if (response.code() == 304) { - pullChatMessages(1) - } else { - processMessages(response, true, finalTimeout) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - - } else { - ncApi.pullChatMessages( - credentials, - ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(3) { observable -> inConversation && !wasDetached } - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(response: Response<*>) { - processMessages(response, false, 0) - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - } - } - } - - private fun processMessages( - response: Response<*>, - isFromTheFuture: Boolean, - timeout: Int - ) { - val xChatLastGivenHeader: String? = response.headers() - .get("X-Chat-Last-Given") - if (response.headers().size() > 0 && !TextUtils.isEmpty(xChatLastGivenHeader)) { - - val header = xChatLastGivenHeader?.toLong() - if (header != null) { - if (isFromTheFuture) { - globalLastKnownFutureMessageId = header - } else { - globalLastKnownPastMessageId = header - } - } - } - - if (response.code() == 200) { - - val chatOverall = response.body() as ChatOverall? - val chatMessageList = chatOverall?.ocs!!.data - - val wasFirstMessageProcessing = isFirstMessagesProcessing - - if (isFirstMessagesProcessing) { - cancelNotificationsForCurrentConversation() - - isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE - - messagesListView?.visibility = View.VISIBLE - - } - - var countGroupedMessages = 0 - if (!isFromTheFuture) { - - for (i in chatMessageList.indices) { - if (chatMessageList.size > i + 1) { - if (TextUtils.isEmpty(chatMessageList[i].systemMessage) && - TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) && - chatMessageList[i + 1].actorId == chatMessageList[i].actorId && - countGroupedMessages < 4 && DateFormatter.isSameDay( - chatMessageList[i].createdAt, - chatMessageList[i + 1].createdAt - ) - ) { - chatMessageList[i].grouped = true - countGroupedMessages++ - } else { - countGroupedMessages = 0 - } - } - - val chatMessage = chatMessageList[i] - chatMessage.oneToOneConversation = - currentConversation?.type == Conversation.ConversationType.ONE_TO_ONE_CONVERSATION - chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed - chatMessage.activeUser = conversationUser - - } - - if (wasFirstMessageProcessing && chatMessageList.size > 0) { - globalLastKnownFutureMessageId = chatMessageList[0].jsonMessageId!! - } - - if (adapter != null) { - adapter?.addToEnd(chatMessageList, false) - } - - } else { - - var chatMessage: ChatMessage - - val shouldAddNewMessagesNotice = - timeout == 0 && adapter?.itemCount ?: 0 > 0 && chatMessageList.size > 0 - - if (shouldAddNewMessagesNotice) { - val unreadChatMessage = ChatMessage() - unreadChatMessage.jsonMessageId = -1 - unreadChatMessage.actorId = "-1" - unreadChatMessage.timestamp = chatMessageList[0].timestamp - unreadChatMessage.message = context.getString(R.string.nc_new_messages) - adapter?.addToStart(unreadChatMessage, false) - } - - val isThereANewNotice = - shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 - - for (i in chatMessageList.indices) { - chatMessage = chatMessageList[i] - - chatMessage.activeUser = conversationUser - chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed - - // if credentials are empty, we're acting as a guest - if (TextUtils.isEmpty(credentials) && myFirstMessage != null && !TextUtils.isEmpty( - myFirstMessage?.toString() - ) - ) { - if (chatMessage.actorType == "guests") { - conversationUser?.userId = chatMessage.actorId!! - setSenderId() - } - } - - val shouldScroll = - !isThereANewNotice && !shouldAddNewMessagesNotice && layoutManager?.findFirstVisibleItemPosition() == 0 || adapter != null && adapter?.itemCount == 0 - - if (!shouldAddNewMessagesNotice && !shouldScroll && popupBubble != null) { - if (!popupBubble!!.isShown) { - newMessagesCount = 1 - popupBubble?.show() - } else if (popupBubble!!.isShown) { - newMessagesCount++ - } - } else { - newMessagesCount = 0 - } - - if (adapter != null) { - chatMessage.grouped = (adapter!!.isPreviousSameAuthor( - chatMessage - .actorId, -1 - ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0) - chatMessage.oneToOneConversation = - (currentConversation?.type == Conversation.ConversationType.ONE_TO_ONE_CONVERSATION) - adapter?.addToStart(chatMessage, shouldScroll) - } - - } - - if (shouldAddNewMessagesNotice && adapter != null && messagesListView != null) { - layoutManager?.scrollToPositionWithOffset( - adapter!!.getMessagePositionByIdInReverse("-1"), messagesListView!!.height / 2 - ) - } - - } - - if (inConversation) { - pullChatMessages(1) - } - } else if (response.code() == 304 && !isFromTheFuture) { - if (isFirstMessagesProcessing) { - cancelNotificationsForCurrentConversation() - - isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE - } - - historyRead = true - - if (!lookingIntoFuture && inConversation) { - pullChatMessages(1) - } - } - } - - override fun onLoadMore( - page: Int, - totalItemsCount: Int - ) { - if (!historyRead && inConversation) { - pullChatMessages(0) - } - } - - override fun format(date: Date): String { - return if (DateFormatter.isToday(date)) { - resources!!.getString(R.string.nc_date_header_today) - } else if (DateFormatter.isYesterday(date)) { - resources!!.getString(R.string.nc_date_header_yesterday) - } else { - DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) - } - } - - override fun onCreateOptionsMenu( - menu: Menu, - inflater: MenuInflater - ) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_conversation, menu) - if (conversationUser?.userId == "?") { - menu.removeItem(R.id.conversation_info) - } else { - conversationInfoMenuItem = menu.findItem(R.id.conversation_info) - conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) - conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) - - loadAvatarForStatusBar() - } - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - conversationUser?.let { - if (it.hasSpreedFeatureCapability("read-only-rooms")) { - checkReadOnlyState() - } - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - router.popCurrentController() - return true - } - R.id.conversation_video_call -> { - if (conversationVideoMenuItem?.icon?.alpha == 255) { - startACall(false) - return true - } - return false - } - R.id.conversation_voice_call -> { - if (conversationVoiceCallMenuItem?.icon?.alpha == 255) { - startACall(true) - return true - } - return false - } - R.id.conversation_info -> { - showConversationInfoScreen() - return true - } - else -> return super.onOptionsItemSelected(item) - } - } - - private fun startACall(isVoiceOnlyCall: Boolean) { - isLeavingForConversation = true - if (!isVoiceOnlyCall) { - val videoCallIntent = getIntentForCall(false) - if (videoCallIntent != null) { - startActivity(videoCallIntent) - } - } else { - val voiceCallIntent = getIntentForCall(true) - if (voiceCallIntent != null) { - startActivity(voiceCallIntent) - } - } - } - - private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { - if (currentConversation != null) { - val bundle = Bundle() - bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomId) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) - bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) - - if (isVoiceOnlyCall) { - bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) - } - - if (activity != null) { - val callIntent = Intent(activity, MagicCallActivity::class.java) - callIntent.putExtras(bundle) - - return callIntent - } else { - return null - } - } else { - return null - } - } - - @OnClick(R.id.cancelReplyButton) - fun cancelReply() { - quotedChatMessageView?.visibility = View.GONE - messageInputView?.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE - messageInputView?.findViewById(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE - } - - override fun onMessageViewLongClick(view: View?, message: IMessage?) { - PopupMenu(this.context, view, if (message?.user?.id == conversationUser?.userId) Gravity.END else Gravity.START).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - setForceShowIcon(true) - } - setOnMenuItemClickListener { item -> - when (item?.itemId) { - - R.id.action_copy_message -> { - val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - val clipData = ClipData.newPlainText(resources?.getString(R.string.nc_app_name), message?.text) - clipboardManager.setPrimaryClip(clipData) - true - } - R.id.action_reply_to_message -> { - val chatMessage = message as ChatMessage? - chatMessage?.let { - messageInputView?.findViewById(R.id.attachmentButton)?.visibility = View.GONE - messageInputView?.findViewById(R.id.attachmentButtonSpace)?.visibility = View.GONE - messageInputView?.findViewById(R.id.cancelReplyButton)?.visibility = View.VISIBLE - messageInputView?.findViewById(R.id.quotedMessage)?.maxLines = 2 - messageInputView?.findViewById(R.id.quotedMessage)?.ellipsize = TextUtils.TruncateAt.END - messageInputView?.findViewById(R.id.quotedMessage)?.text = it.text - messageInputView?.findViewById(R.id.quotedMessageAuthor)?.text = it.actorDisplayName - ?: context.getText(R.string.nc_nick_guest) - - conversationUser?.let { currentUser -> - chatMessage.imageUrl?.let { previewImageUrl -> - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = View.VISIBLE - - val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, resources?.displayMetrics) - messageInputView?.findViewById(R.id.quotedMessageImage)?.maxHeight = px.toInt() - val layoutParams = messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams - layoutParams.flexGrow = 0f - messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams = layoutParams - messageInputView?.findViewById(R.id.quotedMessageImage)?.load(previewImageUrl) { - addHeader("Authorization", currentUser.getCredentials()) - } - } ?: run { - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE - } - } - - quotedChatMessageView?.tag = message?.jsonMessageId - quotedChatMessageView?.visibility = View.VISIBLE - } - true - } - else -> false - } - } - inflate(R.menu.chat_message_menu) - menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable - show() - } - } - - override fun hasContentFor( - message: IMessage, - type: Byte - ): Boolean { - when (type) { - CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) - CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1" - } - - return false - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { - /* - switch (webSocketCommunicationEvent.getType()) { - case "refreshChat": - - if (webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID).equals(Long.toString(conversationUser.getId()))) { - if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) { - pullChatMessages(2); - } - } - break; - default: - }*/ - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { - if (currentConversation?.type != Conversation.ConversationType - .ONE_TO_ONE_CONVERSATION || currentConversation?.name != - userMentionClickEvent.userId - ) { - val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( - conversationUser?.baseUrl, "1", - userMentionClickEvent.userId, null - ) - - ncApi.createRoom( - credentials, - retrofitBucket.url, retrofitBucket.queryMap - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - - } - - override fun onNext(conversationOverall: ConversationOverall) { - val conversationIntent = Intent(activity, MagicCallActivity::class.java) - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, conversationOverall.ocs.data.token) - bundle.putString(BundleKeys.KEY_ROOM_ID, conversationOverall.ocs.data.conversationId) - - if (conversationUser != null) { - if (conversationUser.hasSpreedFeatureCapability("chat-v2")) { - bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(conversationOverall.ocs.data) - ) - conversationIntent.putExtras(bundle) - - if (conversationOverall != null && conversationOverall.ocs != null && conversationOverall.ocs.data != - null && conversationOverall.ocs.data.token != null - ) { - ConductorRemapping.remapChatController( - router, conversationUser.id, - conversationOverall.ocs.data.token!!, bundle, false - ) - } - } - - } else { - conversationIntent.putExtras(bundle) - startActivity(conversationIntent) - Handler().postDelayed({ - if (!isDestroyed && !isBeingDestroyed) { - router.popCurrentController() - } - }, 100) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() {} - }) - } - } - - companion object { - private val TAG = "ChatController" - val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 - val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index b89f18367..a4459dc87 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -69,6 +69,7 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.getCredentials +import com.nextcloud.talk.newarch.local.models.toUser import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils @@ -268,7 +269,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && (conversation!!.type == Conversation.ConversationType.GROUP_CONVERSATION || conversation!!.type == PUBLIC_CONVERSATION) && conversation!!.canModerate( - conversationUser + conversationUser.toUser() ) ) { conversationInfoWebinar.visibility = View.VISIBLE @@ -652,7 +653,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), if (isAttached && (!isBeingDestroyed || !isDestroyed)) { - if (conversationCopy!!.canModerate(conversationUser)) { + if (conversationCopy!!.canModerate(conversationUser.toUser())) { actionTextView.visibility = View.VISIBLE } else { actionTextView.visibility = View.GONE @@ -663,13 +664,13 @@ class ConversationInfoController(args: Bundle) : BaseController(), setupGeneralSettings() setupWebinaryView() - if (!conversation!!.canLeave(conversationUser)) { + if (!conversation!!.canLeave(conversationUser.toUser())) { leaveConversationAction.visibility = View.GONE } else { leaveConversationAction.visibility = View.VISIBLE } - if (!conversation!!.canModerate(conversationUser)) { + if (!conversation!!.canModerate(conversationUser.toUser())) { deleteConversationAction.visibility = View.GONE } else { deleteConversationAction.visibility = View.VISIBLE @@ -709,7 +710,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), if (conversation != null && conversationUser != null) { changeConversationName.value = conversation!!.displayName - if (conversation!!.isNameEditable(conversationUser)) { + if (conversation!!.isNameEditable(conversationUser.toUser())) { changeConversationName.visibility = View.VISIBLE } else { changeConversationName.visibility = View.GONE @@ -873,7 +874,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), ) ) - if (!conversation!!.canModerate(conversationUser)) { + if (!conversation!!.canModerate(conversationUser.toUser())) { items = mutableListOf() } else { if (participant.type == Participant.ParticipantType.MODERATOR || participant.type == Participant.ParticipantType.OWNER) { diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 08d35376c..bf336ef3e 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -289,7 +289,7 @@ class NotificationWorker( } val request = Images().getRequestForUrl( - Coil.loader(), applicationContext, avatarUrl!!, signatureVerification.userEntity, + Coil.loader(), applicationContext, avatarUrl!!, signatureVerification.userEntity!!.toUser(), target, null, CircleCropTransformation() ) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 15b477cda..545e98b05 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -29,7 +29,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.converters.* import com.nextcloud.talk.models.json.participants.Participant -import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.User +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability import kotlinx.serialization.Serializable import lombok.Data import org.parceler.Parcel @@ -152,36 +153,35 @@ class Conversation { return resources.getString(R.string.nc_delete_conversation_default) } - private fun isLockedOneToOne(conversationUser: UserNgEntity): Boolean { + private fun isLockedOneToOne(conversationUser: User): Boolean { return type == ConversationType.ONE_TO_ONE_CONVERSATION && conversationUser .hasSpreedFeatureCapability( "locked-one-to-one-rooms" ) } - fun canModerate(conversationUser: UserNgEntity): Boolean { + fun canModerate(conversationUser: User): Boolean { return (Participant.ParticipantType.OWNER == participantType || Participant.ParticipantType.MODERATOR == participantType) && !isLockedOneToOne( conversationUser ) } - fun shouldShowLobby(conversationUser: UserNgEntity): Boolean { - return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate( - conversationUser + fun shouldShowLobby(conversationUser: User): Boolean { + return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate(conversationUser ) } - fun isLobbyViewApplicable(conversationUser: UserNgEntity): Boolean { + fun isLobbyViewApplicable(conversationUser: User): Boolean { return !canModerate( conversationUser ) && (type == ConversationType.GROUP_CONVERSATION || type == ConversationType.PUBLIC_CONVERSATION) } - fun isNameEditable(conversationUser: UserNgEntity): Boolean { + fun isNameEditable(conversationUser: User): Boolean { return canModerate(conversationUser) && ConversationType.ONE_TO_ONE_CONVERSATION != type } - fun canLeave(conversationUser: UserNgEntity): Boolean { + fun canLeave(conversationUser: User): Boolean { return !canModerate( conversationUser ) || type != ConversationType.ONE_TO_ONE_CONVERSATION && participants!!.size > 1 diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt new file mode 100644 index 000000000..449d144f1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt @@ -0,0 +1,56 @@ +package com.nextcloud.talk.newarch.features.chat + +import android.content.Context +import com.nextcloud.talk.R +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.otaliastudios.elements.Page +import com.otaliastudios.elements.Source +import com.otaliastudios.elements.extensions.HeaderSource +import com.stfalcon.chatkit.utils.DateFormatter +import java.util.* + +class ChatDateHeaderSource(private val context: Context, private val elementType: Int) : HeaderSource() { + // Store the last header that was added, even if it belongs to a previous page. + private var headersAlreadyAdded = mutableListOf() + + override fun dependsOn(source: Source<*>) = source is ChatViewSource + + override fun getElementType(data: Data): Int { + return elementType + } + + override fun areItemsTheSame(first: Data, second: Data): Boolean { + return first == second + } + override fun computeHeaders(page: Page, list: List): List> { + val results = arrayListOf>() + headersAlreadyAdded = mutableListOf() + var dateHeader = "" + for (chatElement in list) { + if (chatElement.data is ChatMessage) { + dateHeader = formatDate(chatElement.data.createdAt) + if (!headersAlreadyAdded.contains(dateHeader)) { + results.add(Data(chatElement, dateHeader)) + headersAlreadyAdded.add(dateHeader) + } + } + } + + return results + } + + private fun formatDate(date: Date): String { + return when { + DateFormatter.isToday(date) -> { + context.getString(R.string.nc_date_header_today) + } + DateFormatter.isYesterday(date) -> { + context.resources.getString(R.string.nc_date_header_yesterday) + } + else -> { + DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt new file mode 100644 index 000000000..a4f85186e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt @@ -0,0 +1,6 @@ +package com.nextcloud.talk.newarch.features.chat + +data class ChatElement( + val data: Any, + val elementType: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt new file mode 100644 index 000000000..7a658879f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt @@ -0,0 +1,11 @@ +package com.nextcloud.talk.newarch.features.chat + +enum class ChatElementTypes { + INCOMING_TEXT_MESSAGE, + OUTGOING_TEXT_MESSAGE, + INCOMING_PREVIEW_MESSAGE, + OUTGOING_PREVIEW_MESSAGE, + SYSTEM_MESSAGE, + UNREAD_MESSAGE_NOTICE, + DATE_HEADER +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt new file mode 100644 index 000000000..e994cb2cc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt @@ -0,0 +1,216 @@ +package com.nextcloud.talk.newarch.features.chat + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import coil.api.loadAny +import coil.api.newLoadBuilder +import com.amulyakhare.textdrawable.TextDrawable +import com.nextcloud.talk.R +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.newarch.features.chat.interfaces.ImageLoaderInterface +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType +import com.otaliastudios.elements.Element +import com.otaliastudios.elements.Page +import com.otaliastudios.elements.Presenter +import com.otaliastudios.elements.extensions.HeaderSource +import com.stfalcon.chatkit.utils.DateFormatter +import kotlinx.android.synthetic.main.item_message_quote.view.* +import kotlinx.android.synthetic.main.rv_chat_incoming_preview_item.view.* +import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.* +import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.messageUserAvatar +import kotlinx.android.synthetic.main.rv_chat_outgoing_preview_item.view.* +import kotlinx.android.synthetic.main.rv_chat_outgoing_text_item.view.* +import kotlinx.android.synthetic.main.rv_chat_system_item.view.* +import kotlinx.android.synthetic.main.rv_date_and_unread_notice_item.view.* +import org.koin.core.KoinComponent + +open class ChatPresenter(context: Context, onElementClick: ((Page, Holder, Element) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter(context, onElementClick), KoinComponent { + override val elementTypes: Collection + get() = listOf(ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal, ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal, ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.SYSTEM_MESSAGE.ordinal, ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal, ChatElementTypes.DATE_HEADER.ordinal) + + override fun onCreate(parent: ViewGroup, elementType: Int): Holder { + return when (elementType) { + ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal -> { + Holder(getLayoutInflater().inflate(R.layout.rv_chat_incoming_text_item, parent, false)) + } + ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal -> { + Holder(getLayoutInflater().inflate(R.layout.rv_chat_outgoing_text_item, parent, false)) + } + ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal -> { + Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false)) + } + ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal -> { + Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false)) + } + ChatElementTypes.SYSTEM_MESSAGE.ordinal -> { + Holder(getLayoutInflater().inflate(R.layout.rv_chat_system_item, parent, false)) + } + else -> { + Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false)) + } + } + } + + override fun onBind(page: Page, holder: Holder, element: Element, payloads: List) { + super.onBind(page, holder, element, payloads) + + holder.itemView.setOnLongClickListener { + onElementLongClick?.invoke(page, holder, element) + true + } + + var chatElement: ChatElement? + var chatMessage: ChatMessage? = null + + if (element.data is ChatElement) { + chatElement = element.data as ChatElement + chatMessage = chatElement.data as ChatMessage? + } + + when { + chatMessage != null -> { + chatMessage.let { + if (element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal || element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal) { + holder.itemView.messageAuthor?.text = it.actorDisplayName + holder.itemView.messageUserAvatar?.isVisible = !it.grouped && !it.oneToOneConversation + + if (element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal) { + holder.itemView.incomingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + holder.itemView.incomingMessageText.text = it.text + + if (it.actorType == "bots" && it.actorId == "changelog") { + holder.itemView.messageUserAvatar.isVisible = true + val layers = arrayOfNulls(2) + layers[0] = context.getDrawable(R.drawable.ic_launcher_background) + layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable)) + imageLoader.getImageLoader().load(loadBuilder.build()) + } else if (it.actorType == "bots") { + holder.itemView.messageUserAvatar.isVisible = true + val drawable = TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRound( + ">", + context.resources.getColor(R.color.black) + ) + val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(drawable)) + imageLoader.getImageLoader().load(loadBuilder.build()) + } else if (!it.grouped && !it.oneToOneConversation) { + holder.itemView.messageUserAvatar.isVisible = true + imageLoader.loadImage(holder.itemView.messageUserAvatar, it.user.avatar) + } else { + holder.itemView.messageUserAvatar.isVisible = false + } + } else { + holder.itemView.outgoingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + holder.itemView.outgoingMessageText.text = it.text + } + + it.parentMessage?.let { parentMessage -> + parentMessage.imageUrl?.let { previewMessageUrl -> + holder.itemView.quotedMessageImage.visibility = View.VISIBLE + imageLoader.loadImage(holder.itemView.quotedMessageImage, previewMessageUrl) + } ?: run { + holder.itemView.quotedMessageImage.visibility = View.GONE + } + + holder.itemView.quotedMessageAuthor.text = parentMessage.actorDisplayName ?: context.getText(R.string.nc_nick_guest) + holder.itemView.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.colorPrimary)) + holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary) + holder.itemView.quotedChatMessageView.visibility = View.VISIBLE + } ?: run { + holder.itemView.quotedChatMessageView.visibility = View.GONE + } + + } else if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) { + var previewAvailable = true + val mutableMap = mutableMapOf() + if (it.selectedIndividualHashMap!!.containsKey("mimetype")) { + mutableMap.put("mimetype", it.selectedIndividualHashMap!!["mimetype"]!!) + if (it.imageUrl == "no-preview") { + previewAvailable = false + imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(chatMessage.selectedIndividualHashMap!!["mimetype"])) + } + } + + // Before someone tells me parts of this can be refactored so there is less code: + // YES, I KNOW! + // But the way it's done now means pretty much anyone can understand it and it's easy + // to modify. Prefer simplicity over complexity wherever possible + + if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal) { + if (previewAvailable) { + imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!) + } + if (!it.grouped && !it.oneToOneConversation) { + holder.itemView.messageUserAvatar.visibility = View.GONE + } else { + holder.itemView.messageUserAvatar.visibility = View.VISIBLE + imageLoader.loadImage(holder.itemView.messageUserAvatar, chatMessage.user.avatar) + } + + when (it.messageType) { + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { + holder.itemView.incomingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"] + } + ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> { + holder.itemView.incomingPreviewMessageText.text = "GIPHY" + } + ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> { + holder.itemView.incomingPreviewMessageText.text = "TENOR" + } + else -> { + holder.itemView.incomingPreviewMessageText.text = "" + } + } + + holder.itemView.incomingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + } else { + if (previewAvailable) { + imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!) + } + + when (it.messageType) { + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { + holder.itemView.outgoingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"] + } + ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> { + holder.itemView.outgoingPreviewMessageText.text = "GIPHY" + } + ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> { + holder.itemView.outgoingPreviewMessageText.text = "TENOR" + } + else -> { + holder.itemView.outgoingPreviewMessageText.text = "" + } + } + + holder.itemView.outgoingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + } + + + } else { + // it's ChatElementTypes.SYSTEM_MESSAGE + holder.itemView.systemMessageText.text = chatMessage.text + holder.itemView.systemItemTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + } + } + } + element.type == ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal -> { + holder.itemView.noticeText.text = context.resources.getString(R.string.nc_new_messages) + } + else -> { + // Date header + holder.itemView.noticeText.text = (element.data as HeaderSource.Data<*, *>).header.toString() + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt index 119c56e44..9c544ee80 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt @@ -22,19 +22,22 @@ package com.nextcloud.talk.newarch.features.chat +import android.content.ComponentName +import android.content.Context +import android.content.Intent import android.content.res.Resources import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.InputFilter -import android.text.TextUtils import android.text.TextWatcher import android.view.* -import android.widget.AbsListView +import android.widget.ImageView import androidx.lifecycle.observe import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import coil.ImageLoader import coil.api.load import coil.target.Target import coil.transform.CircleCropTransformation @@ -43,61 +46,61 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.nextcloud.talk.R -import com.nextcloud.talk.adapters.messages.* import com.nextcloud.talk.callbacks.MentionAutocompleteCallback import com.nextcloud.talk.components.filebrowser.controllers.BrowserController -import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.mention.Mention -import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.features.chat.interfaces.ImageLoaderInterface import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.local.models.getMaxMessageLength import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.utils.Images +import com.nextcloud.talk.newarch.utils.NetworkComponents import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.utils.* +import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID import com.nextcloud.talk.utils.text.Spans import com.otaliastudios.autocomplete.Autocomplete -import com.stfalcon.chatkit.commons.models.IMessage -import com.stfalcon.chatkit.messages.MessageHolders +import com.otaliastudios.elements.Adapter +import com.otaliastudios.elements.Element +import com.otaliastudios.elements.Page +import com.otaliastudios.elements.Presenter +import com.otaliastudios.elements.pagers.PageSizePager import com.stfalcon.chatkit.messages.MessagesListAdapter -import com.stfalcon.chatkit.utils.DateFormatter import com.uber.autodispose.lifecycle.LifecycleScopeProvider import kotlinx.android.synthetic.main.controller_chat.view.* -import kotlinx.android.synthetic.main.conversations_list_view.view.* import kotlinx.android.synthetic.main.lobby_view.view.* import kotlinx.android.synthetic.main.view_message_input.view.* import org.koin.android.ext.android.inject import org.parceler.Parcels import java.util.* -import coil.ImageLoader as CoilImageLoader -import com.stfalcon.chatkit.commons.ImageLoader as ChatKitImageLoader -class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter -.OnMessageLongClickListener, MessagesListAdapter.Formatter { +class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface { override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) override val lifecycleOwner = ControllerLifecycleOwner(this) - lateinit var viewModel: ChatViewModel + private lateinit var viewModel: ChatViewModel val factory: ChatViewModelFactory by inject() - val imageLoader: CoilImageLoader by inject() + private val networkComponents: NetworkComponents by inject() var conversationInfoMenuItem: MenuItem? = null var conversationVoiceCallMenuItem: MenuItem? = null var conversationVideoMenuItem: MenuItem? = null - private var newMessagesCount = 0 - private lateinit var recyclerViewAdapter: MessagesListAdapter private lateinit var mentionAutocomplete: Autocomplete<*> private var shouldShowLobby: Boolean = false private var isReadOnlyConversation: Boolean = false + private lateinit var messagesAdapter: Adapter + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup @@ -105,12 +108,22 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi setHasOptionsMenu(true) actionBar?.show() viewModel = viewModelProvider(factory).get(ChatViewModel::class.java) - viewModel.init(args.getParcelable(BundleKeys.KEY_USER_ENTITY)!!, args.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, args.getString(KEY_CONVERSATION_PASSWORD)) + val view = super.onCreateView(inflater, container) + + viewModel.init(bundle.getParcelable(BundleKeys.KEY_USER)!!, bundle.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, bundle.getString(KEY_CONVERSATION_PASSWORD)) + + messagesAdapter = Adapter.builder(this) + .setPager(PageSizePager(80)) + //.addSource(ChatViewSource(itemsPerPage = 10)) + .addSource(ChatDateHeaderSource(activity as Context, ChatElementTypes.DATE_HEADER.ordinal)) + .addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state)) + .addPresenter(ChatPresenter(activity as Context, ::onElementClick, ::onElementLongClick, this)) + .setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true) + .into(view.messagesRecyclerView) viewModel.apply { conversation.observe(this@ChatView) { conversation -> setTitle() - setupAdapter() if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == conversation?.type) { loadAvatar() @@ -124,36 +137,100 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi activity?.invalidateOptionsMenu() if (shouldShowLobby) { - view?.messagesListView?.visibility = View.GONE - view?.messageInputView?.visibility = View.GONE - view?.lobbyView?.visibility = View.VISIBLE + view.messagesListView?.visibility = View.GONE + view.messageInputView?.visibility = View.GONE + view.lobbyView?.visibility = View.VISIBLE val timer = conversation.lobbyTimer - if (timer != null && timer != 0L) { - view?.lobbyTextView?.text = String.format( + val unit = if (timer != null && timer != 0L) { + view.lobbyTextView?.text = String.format( resources!!.getString(R.string.nc_lobby_waiting_with_date), DateUtils.getLocalDateStringFromTimestampForLobby( conversation.lobbyTimer!! )) } else { - view?.lobbyTextView?.setText(R.string.nc_lobby_waiting) + view.lobbyTextView?.setText(R.string.nc_lobby_waiting) } } else { - view?.messagesListView?.visibility = View.GONE - view?.lobbyView?.visibility = View.GONE + view.messagesListView?.visibility = View.GONE + view.lobbyView?.visibility = View.GONE if (isReadOnlyConversation) { - view?.messageInputView?.visibility = View.GONE + view.messageInputView?.visibility = View.GONE } else { - view?.messageInputView?.visibility = View.VISIBLE - + view.messageInputView?.visibility = View.VISIBLE } } - } } - return super.onCreateView(inflater, container) + return view } + private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element) { + if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) { + element.data?.let { chatElement -> + val chatMessage = chatElement.data as ChatMessage + val currentUser = viewModel.user + if (chatMessage.messageType == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { + val accountString = currentUser.username + "@" + currentUser.baseUrl + .replace("https://", "") + .replace("http://", "") + if (canWeOpenFilesApp(context, accountString)) { + val filesAppIntent = Intent(Intent.ACTION_VIEW, null) + val componentName = ComponentName( + context.getString(R.string.nc_import_accounts_from), + "com.owncloud.android.ui.activity.FileDisplayActivity" + ) + filesAppIntent.component = componentName + filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + filesAppIntent.setPackage( + context.getString(R.string.nc_import_accounts_from) + ) + filesAppIntent.putExtra( + KEY_ACCOUNT, accountString + ) + filesAppIntent.putExtra( + KEY_FILE_ID, + chatMessage.selectedIndividualHashMap!!["id"] + ) + context.startActivity(filesAppIntent) + } else { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(chatMessage.selectedIndividualHashMap!!["link"]) + ) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + } else if (chatMessage.messageType == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://giphy.com") + ) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } else if (chatMessage.messageType == ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://tenor.com") + ) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } else if (chatMessage.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE) { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(chatMessage.imageUrl) + ) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + } + + } + } + + private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element) { + + } override fun onAttach(view: View) { super.onAttach(view) @@ -185,56 +262,15 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi private fun setupViews() { view?.let { view -> - view.recyclerView.initRecyclerView( + view.messagesRecyclerView.initRecyclerView( LinearLayoutManager(view.context), recyclerViewAdapter, false ) - recyclerViewAdapter.setLoadMoreListener(this) - recyclerViewAdapter.setDateHeadersFormatter { format(it) } - recyclerViewAdapter.setOnMessageLongClickListener { onMessageLongClick(it) } - view.popupBubbleView.setRecyclerView(view.messagesListView) - view.popupBubbleView.setPopupBubbleListener { context -> - if (newMessagesCount != 0) { - val scrollPosition: Int - if (newMessagesCount - 1 < 0) { - scrollPosition = 0 - } else { - scrollPosition = newMessagesCount - 1 - } - view.messagesListView.postDelayed({ - view.messagesListView.smoothScrollToPosition(scrollPosition) - }, 200) - } - } - - view.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged( - recyclerView: RecyclerView, - newState: Int - ) { - super.onScrollStateChanged(recyclerView, newState) - - if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - if (newMessagesCount != 0) { - val layoutManager: LinearLayoutManager = view.messagesListView.layoutManager as LinearLayoutManager - if (layoutManager.findFirstCompletelyVisibleItemPosition() < - newMessagesCount - ) { - newMessagesCount = 0 - - view.popupBubbleView?.hide() - } - } - } - } - }) - val filters = arrayOfNulls(1) val lengthFilter = viewModel.user.getMaxMessageLength() - filters[0] = InputFilter.LengthFilter(lengthFilter) view.messageInput.filters = filters @@ -365,9 +401,9 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi viewModel.conversation.value?.let { val bundle = Bundle() bundle.putParcelable( - BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType) + BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType) ) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(viewModel.user)) + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, viewModel.user) bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, it.token) router.pushController( RouterTransaction.with(BrowserController(bundle)) @@ -378,65 +414,8 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi } } - private fun setupAdapter() { - val messageHolders = MessageHolders() - messageHolders.setIncomingTextConfig( - MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message - ) - messageHolders.setOutcomingTextConfig( - MagicOutcomingTextMessageViewHolder::class.java, - R.layout.item_custom_outcoming_text_message - ) - - messageHolders.setIncomingImageConfig( - MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message - ) - messageHolders.setOutcomingImageConfig( - MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message - ) - - messageHolders.registerContentType( - ChatController.CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, - this - ) - - messageHolders.registerContentType( - ChatController.CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this - ) - - recyclerViewAdapter = MessagesListAdapter( - viewModel.user.userId, messageHolders, ChatKitImageLoader { imageView, url, payload -> - imageView.load(url) { - if (url!!.contains("/avatar/")) { - transformations(CircleCropTransformation()) - } else { - if (payload is ImageLoaderPayload) { - payload.map?.let { - if (payload.map.containsKey("mimetype")) { - placeholder( - DrawableUtils.getDrawableResourceIdForMimeType( - payload.map.get("mimetype") as String? - ) - ) - } - } - } - } - - val needsAuthBasedOnUrl = url.contains("index.php/core/preview?fileId=") || url.contains("index.php/avatar/") - if (url.startsWith(viewModel.user.baseUrl) && needsAuthBasedOnUrl) { - addHeader("Authorization", viewModel.user.getCredentials()) - } - } - }) - - } - private fun loadAvatar() { + val imageLoader = networkComponents.getImageLoader(viewModel.user) val avatarSize = DisplayUtils.convertDpToPixel( conversationVoiceCallMenuItem?.icon!! .intrinsicWidth.toFloat(), activity!! @@ -459,9 +438,7 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi ), viewModel.user, target, this, CircleCropTransformation() ) - imageLoader.load(avatarRequest) - } } } @@ -474,35 +451,30 @@ class ChatView : BaseView(), MessageHolders.ContentChecker, MessagesLi return viewModel.conversation.value?.displayName } - override fun hasContentFor(message: IMessage, type: Byte): Boolean { - when (type) { - ChatController.CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) - ChatController.CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1" - } - - return false + override fun getImageLoader(): ImageLoader { + return networkComponents.getImageLoader(viewModel.user) } - override fun format(date: Date): String { - return when { - DateFormatter.isToday(date) -> { - resources!!.getString(R.string.nc_date_header_today) + override fun loadImage(imageView: ImageView, url: String, payload: MutableMap?) { + val imageLoader = networkComponents.getImageLoader(viewModel.user) + + imageLoader.load(activity as Context, url) { + if (url.contains("/avatar/")) { + transformations(CircleCropTransformation()) + } else { + payload?.let { + if (payload.containsKey("mimetype")) { + placeholder(DrawableUtils.getDrawableResourceIdForMimeType(payload["mimetype"]) + ) + } + } } - DateFormatter.isYesterday(date) -> { - resources!!.getString(R.string.nc_date_header_yesterday) - } - else -> { - DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) + + target(imageView) + val needsAuthBasedOnUrl = url.contains("index.php/core/preview?fileId=") || url.contains("index.php/avatar/") + if (url.startsWith(viewModel.user.baseUrl) && needsAuthBasedOnUrl) { + addHeader("Authorization", viewModel.user.getCredentials()) } } } - - override fun onLoadMore(page: Int, totalItemsCount: Int) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun onMessageLongClick(message: IMessage?) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt index f7dfffddd..f761cfec6 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt @@ -32,6 +32,7 @@ import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsReposit import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase +import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalServiceInterface @@ -43,7 +44,7 @@ class ChatViewModel constructor(application: Application, private val conversationsRepository: ConversationsRepository, private val messagesRepository: MessagesRepository, private val globalService: GlobalService) : BaseViewModel(application), GlobalServiceInterface { - lateinit var user: UserNgEntity + lateinit var user: User val conversation: MutableLiveData = MutableLiveData() var initConversation: Conversation? = null val messagesLiveData = Transformations.switchMap(conversation) { @@ -54,10 +55,10 @@ class ChatViewModel constructor(application: Application, var conversationPassword: String? = null - fun init(user: UserNgEntity, conversationToken: String, conversationPassword: String?) { + fun init(user: User, conversationToken: String, conversationPassword: String?) { viewModelScope.launch { this@ChatViewModel.user = user - this@ChatViewModel.initConversation = conversationsRepository.getConversationForUserWithToken(user.id, conversationToken) + this@ChatViewModel.initConversation = conversationsRepository.getConversationForUserWithToken(user.id!!, conversationToken) this@ChatViewModel.conversationPassword = conversationPassword globalService.getConversation(conversationToken, this@ChatViewModel) } @@ -70,7 +71,7 @@ class ChatViewModel constructor(application: Application, override suspend fun gotConversationInfoForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) { if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) { if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) { - this.conversation.value = conversationsRepository.getConversationForUserWithToken(user.id, conversation.token!!) + this.conversation.value = conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!) conversation.token?.let { conversationToken -> globalService.joinConversation(conversationToken, conversationPassword, this) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt new file mode 100644 index 000000000..32ec1b631 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt @@ -0,0 +1,12 @@ +package com.nextcloud.talk.newarch.features.chat + +import androidx.lifecycle.LiveData +import com.nextcloud.talk.newarch.features.contactsflow.ParticipantElement +import com.otaliastudios.elements.extensions.MainSource + +class ChatViewSource(loadingIndicatorsEnabled: Boolean = true, errorIndicatorEnabled: Boolean = false, emptyIndicatorEnabled: Boolean = false) : MainSource(loadingIndicatorsEnabled, errorIndicatorEnabled, emptyIndicatorEnabled) { + override fun areItemsTheSame(first: T, second: T): Boolean { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/interfaces/ImageLoaderInterface.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/interfaces/ImageLoaderInterface.kt new file mode 100644 index 000000000..ae4ba9f77 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/interfaces/ImageLoaderInterface.kt @@ -0,0 +1,9 @@ +package com.nextcloud.talk.newarch.features.chat.interfaces + +import android.widget.ImageView +import coil.ImageLoader + +interface ImageLoaderInterface { + fun getImageLoader(): ImageLoader + fun loadImage(imageView: ImageView, url: String, payload: MutableMap? = null) +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt index 515cbcff5..8a377355b 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt @@ -38,9 +38,9 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.R -import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.newarch.data.presenters.AdvancedEmptyPresenter +import com.nextcloud.talk.newarch.features.chat.ChatView import com.nextcloud.talk.newarch.features.contactsflow.ContactsViewOperationState import com.nextcloud.talk.newarch.features.contactsflow.ParticipantElement import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView @@ -180,7 +180,7 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() { val bundle = Bundle() if (!hasToken || isNewGroupConversation) { bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken) - router.replaceTopController(RouterTransaction.with(ChatController(bundle)) + router.replaceTopController(RouterTransaction.with(ChatView(bundle)) .popChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())) } else { diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt index b56d94d45..7659d91d7 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt @@ -49,6 +49,7 @@ import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.newarch.data.presenters.AdvancedEmptyPresenter import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher +import com.nextcloud.talk.newarch.local.models.toUser import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.utils.ConductorRemapping @@ -265,7 +266,7 @@ class ConversationsListView : BaseView() { ) } - if (conversation.canLeave(viewModel.globalService.currentUserLiveData.value!!)) { + if (conversation.canLeave(viewModel.globalService.currentUserLiveData.value!!.toUser())) { items.add( BasicListItemWithImage( drawable.ic_exit_to_app_black_24dp, context.getString @@ -274,7 +275,7 @@ class ConversationsListView : BaseView() { ) } - if (conversation.canModerate(viewModel.globalService.currentUserLiveData.value!!)) { + if (conversation.canModerate(viewModel.globalService.currentUserLiveData.value!!.toUser())) { items.add( BasicListItemWithImage( drawable.ic_delete_grey600_24dp, context.getString( diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/User.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/User.kt index 288d0c514..fa64c511c 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/User.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/User.kt @@ -5,6 +5,7 @@ import com.nextcloud.talk.models.json.capabilities.Capabilities import com.nextcloud.talk.models.json.push.PushConfiguration import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings import com.nextcloud.talk.newarch.local.models.other.UserStatus +import com.nextcloud.talk.utils.ApiUtils import kotlinx.android.parcel.Parcelize import kotlinx.serialization.Serializable @@ -24,6 +25,16 @@ data class User( var status: UserStatus? = null ) : Parcelable +fun User.getMaxMessageLength(): Int { + return capabilities?.spreedCapability?.config?.get("chat")?.get("max-length")?.toInt() ?: 1000 +} + +fun User.getCredentials(): String = ApiUtils.getCredentials(username, token) + +fun User.hasSpreedFeatureCapability(capabilityName: String): Boolean { + return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false +} + fun User.toUserEntity(): UserNgEntity { var userNgEntity: UserNgEntity? = null this.id?.let { diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt index 8f49de3ac..ed95efd1a 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt @@ -91,11 +91,3 @@ fun UserNgEntity.toUser(): User { } fun UserNgEntity.getCredentials(): String = ApiUtils.getCredentials(username, token) - -fun UserNgEntity.hasSpreedFeatureCapability(capabilityName: String): Boolean { - return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false -} - -fun UserNgEntity.getMaxMessageLength(): Int { - return capabilities?.spreedCapability?.config?.get("chat")?.get("max-length")?.toInt() ?: 1000 -} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/services/CallService.kt b/app/src/main/java/com/nextcloud/talk/newarch/services/CallService.kt index a4b5920b6..bb224ef45 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/services/CallService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/services/CallService.kt @@ -157,7 +157,7 @@ class CallService : Service(), KoinComponent, CoroutineScope { .setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString())) .setAutoCancel(true) .setOngoing(true) - .addAction(R.drawable.ic_call_end_white_24px, resources.getString(R.string.reject_call), rejectCallPendingIntent) + .addAction(R.drawable.ic_call_end_white_24px, resources.getString(R.string.nc_reject_call), rejectCallPendingIntent) .setContentIntent(fullScreenPendingIntent) .setFullScreenIntent(fullScreenPendingIntent, true) .setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences), AudioManager.STREAM_RING) @@ -184,7 +184,7 @@ class CallService : Service(), KoinComponent, CoroutineScope { val imageLoader = networkComponents.getImageLoader(signatureVerification.userEntity!!.toUser()) val request = Images().getRequestForUrl( - imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity, + imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity!!.toUser(), target, null, CircleCropTransformation()) imageLoader.load(request) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt index 37bea3572..7ccfe8fd4 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt @@ -36,6 +36,7 @@ import coil.target.Target import coil.transform.Transformation import com.nextcloud.talk.R import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.utils.DisplayUtils @@ -46,7 +47,7 @@ class Images { context: Context, url: String, userEntity: - UserNgEntity?, + User?, target: Target?, lifecycleOwner: LifecycleOwner?, vararg transformations: Transformation diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt index 36fa146d9..f87e83d57 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt @@ -24,7 +24,7 @@ import android.os.Bundle import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler -import com.nextcloud.talk.controllers.ChatController +import com.nextcloud.talk.newarch.features.chat.ChatView object ConductorRemapping { fun remapChatController( @@ -51,13 +51,13 @@ object ConductorRemapping { } else { if (!replaceTop) { router.pushController( - RouterTransaction.with(ChatController(bundle)) + RouterTransaction.with(ChatView(bundle)) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler()).tag(tag) ) } else { router.replaceTopController( - RouterTransaction.with(ChatController(bundle)) + RouterTransaction.with(ChatView(bundle)) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler()).tag(tag) ) diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt index 45c480f8c..3469b0e48 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt @@ -61,6 +61,7 @@ import com.google.android.material.chip.ChipDrawable import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.events.UserMentionClickEvent +import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.utils.text.Spans @@ -186,7 +187,7 @@ object DisplayUtils { context: Context, id: String, label: CharSequence, - conversationUser: UserNgEntity, + conversationUser: User, type: String, @XmlRes chipResource: Int, emojiEditText: EditText? @@ -264,7 +265,7 @@ object DisplayUtils { id: String, label: String, type: String, - conversationUser: UserNgEntity, + conversationUser: User, @XmlRes chipXmlRes: Int ): Spannable { diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 6a1814017..0c84e8b4c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -36,6 +36,7 @@ object BundleKeys { val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD" val KEY_CONVERSATION_TOKEN = "KEY_CONVERSATION_TOKEN" val KEY_USER_ENTITY = "KEY_USER_ENTITY" + val KEY_USER = "KEY_USER" val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION" val KEY_NEW_GROUP_CONVERSATION = "KEY_NEW_GROUP_CONVERSATION" val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS" diff --git a/app/src/main/res/layout/controller_chat.xml b/app/src/main/res/layout/controller_chat.xml index e7c0546ca..16af450bb 100644 --- a/app/src/main/res/layout/controller_chat.xml +++ b/app/src/main/res/layout/controller_chat.xml @@ -72,6 +72,14 @@ app:inputTextSize="16sp" app:showAttachmentButton="true" /> + + - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/rv_chat_incoming_preview_item.xml b/app/src/main/res/layout/rv_chat_incoming_preview_item.xml new file mode 100644 index 000000000..7d42dabdb --- /dev/null +++ b/app/src/main/res/layout/rv_chat_incoming_preview_item.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_text_message.xml b/app/src/main/res/layout/rv_chat_incoming_text_item.xml similarity index 94% rename from app/src/main/res/layout/item_custom_incoming_text_message.xml rename to app/src/main/res/layout/rv_chat_incoming_text_item.xml index 14214ec67..18a26cd08 100644 --- a/app/src/main/res/layout/item_custom_incoming_text_message.xml +++ b/app/src/main/res/layout/rv_chat_incoming_text_item.xml @@ -57,7 +57,7 @@ diff --git a/app/src/main/res/layout/item_custom_outcoming_preview_message.xml b/app/src/main/res/layout/rv_chat_outgoing_preview_item.xml similarity index 94% rename from app/src/main/res/layout/item_custom_outcoming_preview_message.xml rename to app/src/main/res/layout/rv_chat_outgoing_preview_item.xml index 03132625e..8a38e3330 100644 --- a/app/src/main/res/layout/item_custom_outcoming_preview_message.xml +++ b/app/src/main/res/layout/rv_chat_outgoing_preview_item.xml @@ -41,7 +41,7 @@ app:justifyContent="flex_end"> diff --git a/app/src/main/res/layout/item_system_message.xml b/app/src/main/res/layout/rv_chat_system_item.xml similarity index 96% rename from app/src/main/res/layout/item_system_message.xml rename to app/src/main/res/layout/rv_chat_system_item.xml index 06faad0a5..1405fd64a 100644 --- a/app/src/main/res/layout/item_system_message.xml +++ b/app/src/main/res/layout/rv_chat_system_item.xml @@ -40,7 +40,7 @@ app:justifyContent="flex_end"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index ef0c37cc9..828faecc9 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -313,5 +313,5 @@ Vyhledat další účastníky Nová skupina Kam se všichni schovali? - Odmítnout + Odmítnout diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cca17e774..ad20e187b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -320,5 +320,5 @@ Meeting ist für %1$s geplant. Weitere Teilnehmer suchen Neue Gruppe Wo haben sie sich alle versteckt? - Ablehnen + Ablehnen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a84bc60e7..7fd07e25a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -318,5 +318,5 @@ Αναζήτηση περισσότερων συμμετεχόντων Νέα ομάδα Πού κρύβονταν όλοι; - Απόρριψη + Απόρριψη diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7472bee4f..6eca4fba0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -316,5 +316,5 @@ Buscar más participantes Nuevo grupo ¿Dónde se escondieron todos? - Rechazar + Rechazar diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e654afc85..56beba647 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -321,5 +321,5 @@ Bilatu parte-hartzaile gehiago Talde berria Non ezkutatu dira denak? - Baztertu + Baztertu diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 28c8e4ed3..59949da01 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -270,5 +270,5 @@ Etsi lisää osallistujia Uusi ryhmä Mihin he kaikki piiloutuivat? - Hylkää + Hylkää diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2aacbe52b..7fc012dbd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -316,5 +316,5 @@ Le démarrage de cette réunion est prévu à %1$s. Rechercher d\'autres participants Nouveau groupe Où se cachent-ils tous ? - Refuser + Refuser diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 4128221bb..07e8f0e4c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -322,5 +322,5 @@ móbiles. Pode tentar unirse á chamada empregando o navegador web. Buscar máis participantes Grupo novo Onde se agocharon todos? - Rexeitar + Rexeitar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9661a7b0a..5360ac813 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -320,5 +320,5 @@ Cerca altri partecipanti Nuovo gruppo Dove si sono nascosti? - Rifiuta + Rifiuta diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 84006cdb9..6290f95bd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -319,5 +319,5 @@ Je kunt proberen om aan het gesprek deel te nemen via een browser. Zoek meer deelnemers Nieuwe groep Waar zijn ze allemaal naar toe? - Afwijzen + Afwijzen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index edcc6cad1..181c73a75 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -320,5 +320,5 @@ Wyszukaj więcej uczestników Nowa grupa Gdzie oni wszyscy się schowali? - Odrzuć + Odrzuć diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 001481b79..0a8b85eec 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -320,5 +320,5 @@ Procurar por mais participantes Novo grupo Onde eles todos se esconderam? - Rejeitar + Rejeitar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b4cb05658..497abe3ff 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -315,5 +315,5 @@ Искать дополнительных участников Новая группа Где все? - Отклонить + Отклонить diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index c485f5d14..76dc439a9 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -320,5 +320,5 @@ v zozname rozhovorov Vyhľadanie ďalších účastníkov Nová skupina Kde sa všetci schovali? - Odmietnuť + Odmietnuť diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index de4f3f5f1..68fd5020b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -320,5 +320,5 @@ Pozdravite prijatelje in znance. Poišči več udeležencev Nova skupina Kam so se vsi skrili? - Zavrni + Zavrni diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index edac0a7be..f1f7c7089 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -319,5 +319,5 @@ Тражи још учесника Нова група Где су сви нестали? - Одбиј + Одбиј diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index af1c47eae..ecee71060 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -320,5 +320,5 @@ Sök efter fler deltagare Ny grupp Var gömmer sig alla? - Avvisa + Avvisa diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f104fe963..952822eac 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -320,5 +320,5 @@ Başka katılımcılar arayın Yeni grup Hepsi nereye gizlendiler? - Reddet + Reddet diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4fdcbc62f..0f0373b3f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -316,5 +316,5 @@ 搜索更多参与者 新建群组 他们都藏在哪里? - 拒绝 + 拒绝 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 249689384..389d6f79e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,5 +344,5 @@ Search for more participants New group Where did they all hide? - Reject + Reject