New chat - baby steps

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-03-17 18:34:43 +01:00
parent cb2696871d
commit e305979cdd
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
63 changed files with 650 additions and 2662 deletions

View File

@ -155,7 +155,7 @@ android {
ext { ext {
work_version = '2.3.3' work_version = '2.3.3'
koin_version = "2.1.0-alpha-1" koin_version = "2.1.4"
lifecycle_version = '2.2.0' lifecycle_version = '2.2.0'
coil_version = "0.9.5" coil_version = "0.9.5"
room_version = "2.2.4" room_version = "2.2.4"

View File

@ -1,30 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.RawValue
@Parcelize
data class ImageLoaderPayload(
val map: @RawValue HashMap<String, Any>?
) : Parcelable

View File

@ -1,231 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.content.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<ChatMessage>(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<Drawable>(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
}
}
}

View File

@ -1,164 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.content.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<ChatMessage>(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<String, HashMap<String, String>>? = 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<String, String>? = 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
}
}

View File

@ -1,216 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.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<ChatMessage>(
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<Drawable?>(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<ReadFilesystemOperation?> {
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<BrowserFile>
if (browserFileList.isNotEmpty()) {
image.load(getDrawableResourceIdForMimeType(browserFileList[0].mimeType))
}
}
}
override fun onError(e: Throwable) {}
})
}
override fun getPayloadForImageLoader(message: ChatMessage): Any {
val map = HashMap<String, Any>()
// 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!!)
}
}

View File

@ -1,81 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.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<ChatMessage>(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<String, String>? = 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)
}
}

View File

@ -1,52 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters.messages;
import android.view.View;
import com.nextcloud.talk.models.json.chat.ChatMessage;
import com.stfalcon.chatkit.messages.MessageHolders;
public class MagicUnreadNoticeMessageViewHolder
extends MessageHolders.SystemMessageViewHolder<ChatMessage> {
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() {
}
}

View File

@ -176,7 +176,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
//endregion //endregion
//region Protected methods //region Protected methods
protected fun startKoin() { private fun startKoin() {
startKoin { startKoin {
androidContext(this@NextcloudTalkApplication) androidContext(this@NextcloudTalkApplication)
androidLogger() androidLogger()

View File

@ -27,6 +27,7 @@ import android.widget.EditText;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.models.json.mention.Mention; 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.newarch.local.models.UserNgEntity;
import com.nextcloud.talk.utils.BetterImageSpan; import com.nextcloud.talk.utils.BetterImageSpan;
import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DisplayUtils;
@ -38,10 +39,10 @@ import com.vanniktech.emoji.EmojiUtils;
public class MentionAutocompleteCallback implements AutocompleteCallback<Mention> { public class MentionAutocompleteCallback implements AutocompleteCallback<Mention> {
private Context context; private Context context;
private UserNgEntity conversationUser; private User conversationUser;
private EditText editText; private EditText editText;
public MentionAutocompleteCallback(Context context, UserNgEntity conversationUser, public MentionAutocompleteCallback(Context context, User conversationUser,
EditText editText) { EditText editText) {
this.context = context; this.context = context;
this.conversationUser = conversationUser; this.conversationUser = conversationUser;

View File

@ -32,6 +32,7 @@ import coil.api.load
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.components.filebrowser.models.BrowserFile import com.nextcloud.talk.components.filebrowser.models.BrowserFile
import com.nextcloud.talk.interfaces.SelectionInterface 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.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
@ -47,7 +48,7 @@ import org.koin.core.inject
class BrowserFileItem( class BrowserFileItem(
val model: BrowserFile, val model: BrowserFile,
private val activeUser: UserNgEntity, private val activeUser: User,
private val selectionInterface: SelectionInterface private val selectionInterface: SelectionInterface
) : AbstractFlexibleItem<BrowserFileItem.ViewHolder>(), IFilterable<String>, KoinComponent { ) : AbstractFlexibleItem<BrowserFileItem.ViewHolder>(), IFilterable<String>, KoinComponent {
val context: Context by inject() val context: Context by inject()

View File

@ -40,6 +40,7 @@ import com.nextcloud.talk.components.filebrowser.operations.ListingAbstractClass
import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.interfaces.SelectionInterface import com.nextcloud.talk.interfaces.SelectionInterface
import com.nextcloud.talk.jobs.ShareOperationWorker 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.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import eu.davidea.fastscroller.FastScroller import eu.davidea.fastscroller.FastScroller
@ -83,13 +84,13 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex
private var listingAbstractClass: ListingAbstractClass? = null private var listingAbstractClass: ListingAbstractClass? = null
private val browserType: BrowserType private val browserType: BrowserType
private var currentPath: String? = null private var currentPath: String? = null
private val activeUser: UserNgEntity private val activeUser: User
private val roomToken: String? private val roomToken: String?
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
browserType = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_BROWSER_TYPE)) 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) roomToken = args.getString(BundleKeys.KEY_CONVERSATION_TOKEN)
currentPath = "/" currentPath = "/"
@ -130,7 +131,7 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex
iterator.remove() iterator.remove()
if (paths.size == 10 || !iterator.hasNext()) { if (paths.size == 10 || !iterator.hasNext()) {
data = Data.Builder() 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) .putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken)
.putStringArray(BundleKeys.KEY_FILE_PATHS, paths.toTypedArray()) .putStringArray(BundleKeys.KEY_FILE_PATHS, paths.toTypedArray())
.build() .build()

View File

@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface;
import com.nextcloud.talk.components.filebrowser.models.DavResponse; import com.nextcloud.talk.components.filebrowser.models.DavResponse;
import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation; 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 com.nextcloud.talk.newarch.local.models.UserNgEntity;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -43,7 +44,7 @@ public class DavListing extends ListingAbstractClass {
} }
@Override @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<ReadFilesystemOperation>() { Single.fromCallable(new Callable<ReadFilesystemOperation>() {
@Override @Override
public ReadFilesystemOperation call() { public ReadFilesystemOperation call() {

View File

@ -25,7 +25,7 @@ import android.os.Handler;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; 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; import okhttp3.OkHttpClient;
@ -38,7 +38,7 @@ public abstract class ListingAbstractClass {
this.listingInterface = listingInterface; this.listingInterface = listingInterface;
} }
public abstract void getFiles(String path, UserNgEntity currentUser, public abstract void getFiles(String path, User currentUser,
@Nullable OkHttpClient okHttpClient); @Nullable OkHttpClient okHttpClient);
public void cancelAllJobs() { public void cancelAllJobs() {

View File

@ -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.BrowserFile;
import com.nextcloud.talk.components.filebrowser.models.DavResponse; 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.local.models.UserNgEntity;
import com.nextcloud.talk.newarch.utils.NetworkUtils; import com.nextcloud.talk.newarch.utils.NetworkUtils;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
@ -44,7 +45,7 @@ public class ReadFilesystemOperation {
private final int depth; private final int depth;
private final String basePath; private final String basePath;
public ReadFilesystemOperation(OkHttpClient okHttpClient, UserNgEntity currentUser, String path, public ReadFilesystemOperation(OkHttpClient okHttpClient, User currentUser, String path,
int depth) { int depth) {
OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder(); OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder();
okHttpClientBuilder.followRedirects(false); okHttpClientBuilder.followRedirects(false);

View File

@ -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.features.contactsflow.contacts.ContactsView
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials 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.newarch.utils.Images
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
@ -268,7 +269,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && (conversation!!.type if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && (conversation!!.type
== Conversation.ConversationType.GROUP_CONVERSATION || conversation!!.type == == Conversation.ConversationType.GROUP_CONVERSATION || conversation!!.type ==
PUBLIC_CONVERSATION) && conversation!!.canModerate( PUBLIC_CONVERSATION) && conversation!!.canModerate(
conversationUser conversationUser.toUser()
) )
) { ) {
conversationInfoWebinar.visibility = View.VISIBLE conversationInfoWebinar.visibility = View.VISIBLE
@ -652,7 +653,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
if (isAttached && (!isBeingDestroyed || !isDestroyed)) { if (isAttached && (!isBeingDestroyed || !isDestroyed)) {
if (conversationCopy!!.canModerate(conversationUser)) { if (conversationCopy!!.canModerate(conversationUser.toUser())) {
actionTextView.visibility = View.VISIBLE actionTextView.visibility = View.VISIBLE
} else { } else {
actionTextView.visibility = View.GONE actionTextView.visibility = View.GONE
@ -663,13 +664,13 @@ class ConversationInfoController(args: Bundle) : BaseController(),
setupGeneralSettings() setupGeneralSettings()
setupWebinaryView() setupWebinaryView()
if (!conversation!!.canLeave(conversationUser)) { if (!conversation!!.canLeave(conversationUser.toUser())) {
leaveConversationAction.visibility = View.GONE leaveConversationAction.visibility = View.GONE
} else { } else {
leaveConversationAction.visibility = View.VISIBLE leaveConversationAction.visibility = View.VISIBLE
} }
if (!conversation!!.canModerate(conversationUser)) { if (!conversation!!.canModerate(conversationUser.toUser())) {
deleteConversationAction.visibility = View.GONE deleteConversationAction.visibility = View.GONE
} else { } else {
deleteConversationAction.visibility = View.VISIBLE deleteConversationAction.visibility = View.VISIBLE
@ -709,7 +710,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
if (conversation != null && conversationUser != null) { if (conversation != null && conversationUser != null) {
changeConversationName.value = conversation!!.displayName changeConversationName.value = conversation!!.displayName
if (conversation!!.isNameEditable(conversationUser)) { if (conversation!!.isNameEditable(conversationUser.toUser())) {
changeConversationName.visibility = View.VISIBLE changeConversationName.visibility = View.VISIBLE
} else { } else {
changeConversationName.visibility = View.GONE changeConversationName.visibility = View.GONE
@ -873,7 +874,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
) )
) )
if (!conversation!!.canModerate(conversationUser)) { if (!conversation!!.canModerate(conversationUser.toUser())) {
items = mutableListOf() items = mutableListOf()
} else { } else {
if (participant.type == Participant.ParticipantType.MODERATOR || participant.type == Participant.ParticipantType.OWNER) { if (participant.type == Participant.ParticipantType.MODERATOR || participant.type == Participant.ParticipantType.OWNER) {

View File

@ -289,7 +289,7 @@ class NotificationWorker(
} }
val request = Images().getRequestForUrl( val request = Images().getRequestForUrl(
Coil.loader(), applicationContext, avatarUrl!!, signatureVerification.userEntity, Coil.loader(), applicationContext, avatarUrl!!, signatureVerification.userEntity!!.toUser(),
target, null, CircleCropTransformation() target, null, CircleCropTransformation()
) )

View File

@ -29,7 +29,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.converters.* import com.nextcloud.talk.models.json.converters.*
import com.nextcloud.talk.models.json.participants.Participant 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 kotlinx.serialization.Serializable
import lombok.Data import lombok.Data
import org.parceler.Parcel import org.parceler.Parcel
@ -152,36 +153,35 @@ class Conversation {
return resources.getString(R.string.nc_delete_conversation_default) 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 return type == ConversationType.ONE_TO_ONE_CONVERSATION && conversationUser
.hasSpreedFeatureCapability( .hasSpreedFeatureCapability(
"locked-one-to-one-rooms" "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( return (Participant.ParticipantType.OWNER == participantType || Participant.ParticipantType.MODERATOR == participantType) && !isLockedOneToOne(
conversationUser conversationUser
) )
} }
fun shouldShowLobby(conversationUser: UserNgEntity): Boolean { fun shouldShowLobby(conversationUser: User): Boolean {
return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate( return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate(conversationUser
conversationUser
) )
} }
fun isLobbyViewApplicable(conversationUser: UserNgEntity): Boolean { fun isLobbyViewApplicable(conversationUser: User): Boolean {
return !canModerate( return !canModerate(
conversationUser conversationUser
) && (type == ConversationType.GROUP_CONVERSATION || type == ConversationType.PUBLIC_CONVERSATION) ) && (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 return canModerate(conversationUser) && ConversationType.ONE_TO_ONE_CONVERSATION != type
} }
fun canLeave(conversationUser: UserNgEntity): Boolean { fun canLeave(conversationUser: User): Boolean {
return !canModerate( return !canModerate(
conversationUser conversationUser
) || type != ConversationType.ONE_TO_ONE_CONVERSATION && participants!!.size > 1 ) || type != ConversationType.ONE_TO_ONE_CONVERSATION && participants!!.size > 1

View File

@ -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<ChatElement, String>() {
// Store the last header that was added, even if it belongs to a previous page.
private var headersAlreadyAdded = mutableListOf<String>()
override fun dependsOn(source: Source<*>) = source is ChatViewSource
override fun getElementType(data: Data<ChatElement, String>): Int {
return elementType
}
override fun areItemsTheSame(first: Data<ChatElement, String>, second: Data<ChatElement, String>): Boolean {
return first == second
}
override fun computeHeaders(page: Page, list: List<ChatElement>): List<Data<ChatElement, String>> {
val results = arrayListOf<Data<ChatElement, String>>()
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)
}
}
}
}

View File

@ -0,0 +1,6 @@
package com.nextcloud.talk.newarch.features.chat
data class ChatElement(
val data: Any,
val elementType: Int
)

View File

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

View File

@ -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<T : Any>(context: Context, onElementClick: ((Page, Holder, Element<T>) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element<T>) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter<T>(context, onElementClick), KoinComponent {
override val elementTypes: Collection<Int>
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<T>, payloads: List<Any>) {
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<Drawable>(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<String, String>()
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()
}
}
}
}

View File

@ -22,19 +22,22 @@
package com.nextcloud.talk.newarch.features.chat 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.content.res.Resources
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.InputFilter import android.text.InputFilter
import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
import android.view.* import android.view.*
import android.widget.AbsListView import android.widget.ImageView
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import coil.ImageLoader
import coil.api.load import coil.api.load
import coil.target.Target import coil.target.Target
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
@ -43,61 +46,61 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.*
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
import com.nextcloud.talk.components.filebrowser.controllers.BrowserController 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.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.mention.Mention 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.getCredentials
import com.nextcloud.talk.newarch.local.models.getMaxMessageLength import com.nextcloud.talk.newarch.local.models.getMaxMessageLength
import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.newarch.utils.Images
import com.nextcloud.talk.newarch.utils.NetworkComponents
import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.utils.* 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
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_CONVERSATION_PASSWORD
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage import com.otaliastudios.elements.Adapter
import com.stfalcon.chatkit.messages.MessageHolders 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.messages.MessagesListAdapter
import com.stfalcon.chatkit.utils.DateFormatter
import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.controller_chat.view.* 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.lobby_view.view.*
import kotlinx.android.synthetic.main.view_message_input.view.* import kotlinx.android.synthetic.main.view_message_input.view.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.parceler.Parcels import org.parceler.Parcels
import java.util.* import java.util.*
import coil.ImageLoader as CoilImageLoader
import com.stfalcon.chatkit.commons.ImageLoader as ChatKitImageLoader
class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
.OnMessageLongClickListener<IMessage>, MessagesListAdapter.Formatter<Date> {
override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this)
override val lifecycleOwner = ControllerLifecycleOwner(this) override val lifecycleOwner = ControllerLifecycleOwner(this)
lateinit var viewModel: ChatViewModel private lateinit var viewModel: ChatViewModel
val factory: ChatViewModelFactory by inject() val factory: ChatViewModelFactory by inject()
val imageLoader: CoilImageLoader by inject() private val networkComponents: NetworkComponents by inject()
var conversationInfoMenuItem: MenuItem? = null var conversationInfoMenuItem: MenuItem? = null
var conversationVoiceCallMenuItem: MenuItem? = null var conversationVoiceCallMenuItem: MenuItem? = null
var conversationVideoMenuItem: MenuItem? = null var conversationVideoMenuItem: MenuItem? = null
private var newMessagesCount = 0
private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage> private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage>
private lateinit var mentionAutocomplete: Autocomplete<*> private lateinit var mentionAutocomplete: Autocomplete<*>
private var shouldShowLobby: Boolean = false private var shouldShowLobby: Boolean = false
private var isReadOnlyConversation: Boolean = false private var isReadOnlyConversation: Boolean = false
private lateinit var messagesAdapter: Adapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup container: ViewGroup
@ -105,12 +108,22 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
setHasOptionsMenu(true) setHasOptionsMenu(true)
actionBar?.show() actionBar?.show()
viewModel = viewModelProvider(factory).get(ChatViewModel::class.java) 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 { viewModel.apply {
conversation.observe(this@ChatView) { conversation -> conversation.observe(this@ChatView) { conversation ->
setTitle() setTitle()
setupAdapter()
if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == conversation?.type) { if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == conversation?.type) {
loadAvatar() loadAvatar()
@ -124,36 +137,100 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
if (shouldShowLobby) { if (shouldShowLobby) {
view?.messagesListView?.visibility = View.GONE view.messagesListView?.visibility = View.GONE
view?.messageInputView?.visibility = View.GONE view.messageInputView?.visibility = View.GONE
view?.lobbyView?.visibility = View.VISIBLE view.lobbyView?.visibility = View.VISIBLE
val timer = conversation.lobbyTimer val timer = conversation.lobbyTimer
if (timer != null && timer != 0L) { val unit = if (timer != null && timer != 0L) {
view?.lobbyTextView?.text = String.format( view.lobbyTextView?.text = String.format(
resources!!.getString(R.string.nc_lobby_waiting_with_date), resources!!.getString(R.string.nc_lobby_waiting_with_date),
DateUtils.getLocalDateStringFromTimestampForLobby( DateUtils.getLocalDateStringFromTimestampForLobby(
conversation.lobbyTimer!! conversation.lobbyTimer!!
)) ))
} else { } else {
view?.lobbyTextView?.setText(R.string.nc_lobby_waiting) view.lobbyTextView?.setText(R.string.nc_lobby_waiting)
} }
} else { } else {
view?.messagesListView?.visibility = View.GONE view.messagesListView?.visibility = View.GONE
view?.lobbyView?.visibility = View.GONE view.lobbyView?.visibility = View.GONE
if (isReadOnlyConversation) { if (isReadOnlyConversation) {
view?.messageInputView?.visibility = View.GONE view.messageInputView?.visibility = View.GONE
} else { } 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<ChatElement>) {
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<ChatElement>) {
}
override fun onAttach(view: View) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
@ -185,56 +262,15 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
private fun setupViews() { private fun setupViews() {
view?.let { view -> view?.let { view ->
view.recyclerView.initRecyclerView( view.messagesRecyclerView.initRecyclerView(
LinearLayoutManager(view.context), recyclerViewAdapter, false LinearLayoutManager(view.context), recyclerViewAdapter, false
) )
recyclerViewAdapter.setLoadMoreListener(this)
recyclerViewAdapter.setDateHeadersFormatter { format(it) }
recyclerViewAdapter.setOnMessageLongClickListener { onMessageLongClick(it) }
view.popupBubbleView.setRecyclerView(view.messagesListView) 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<InputFilter>(1) val filters = arrayOfNulls<InputFilter>(1)
val lengthFilter = viewModel.user.getMaxMessageLength() val lengthFilter = viewModel.user.getMaxMessageLength()
filters[0] = InputFilter.LengthFilter(lengthFilter) filters[0] = InputFilter.LengthFilter(lengthFilter)
view.messageInput.filters = filters view.messageInput.filters = filters
@ -365,9 +401,9 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
viewModel.conversation.value?.let { viewModel.conversation.value?.let {
val bundle = Bundle() val bundle = Bundle()
bundle.putParcelable( bundle.putParcelable(
BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap<BrowserController.BrowserType>(browserType) BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType)
) )
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap<UserNgEntity>(viewModel.user)) bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, viewModel.user)
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, it.token) bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, it.token)
router.pushController( router.pushController(
RouterTransaction.with(BrowserController(bundle)) RouterTransaction.with(BrowserController(bundle))
@ -378,65 +414,8 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, 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() { private fun loadAvatar() {
val imageLoader = networkComponents.getImageLoader(viewModel.user)
val avatarSize = DisplayUtils.convertDpToPixel( val avatarSize = DisplayUtils.convertDpToPixel(
conversationVoiceCallMenuItem?.icon!! conversationVoiceCallMenuItem?.icon!!
.intrinsicWidth.toFloat(), activity!! .intrinsicWidth.toFloat(), activity!!
@ -459,9 +438,7 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
), viewModel.user, target, this, ), viewModel.user, target, this,
CircleCropTransformation() CircleCropTransformation()
) )
imageLoader.load(avatarRequest) imageLoader.load(avatarRequest)
} }
} }
} }
@ -474,35 +451,30 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
return viewModel.conversation.value?.displayName return viewModel.conversation.value?.displayName
} }
override fun hasContentFor(message: IMessage, type: Byte): Boolean { override fun getImageLoader(): ImageLoader {
when (type) { return networkComponents.getImageLoader(viewModel.user)
ChatController.CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)
ChatController.CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1"
}
return false
} }
override fun format(date: Date): String { override fun loadImage(imageView: ImageView, url: String, payload: MutableMap<String, String>?) {
return when { val imageLoader = networkComponents.getImageLoader(viewModel.user)
DateFormatter.isToday(date) -> {
resources!!.getString(R.string.nc_date_header_today) 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) target(imageView)
} val needsAuthBasedOnUrl = url.contains("index.php/core/preview?fileId=") || url.contains("index.php/avatar/")
else -> { if (url.startsWith(viewModel.user.baseUrl) && needsAuthBasedOnUrl) {
DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) 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.
}
} }

View File

@ -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.repository.offline.MessagesRepository
import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase 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.local.models.UserNgEntity
import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.services.GlobalServiceInterface import com.nextcloud.talk.newarch.services.GlobalServiceInterface
@ -43,7 +44,7 @@ class ChatViewModel constructor(application: Application,
private val conversationsRepository: ConversationsRepository, private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface { private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface {
lateinit var user: UserNgEntity lateinit var user: User
val conversation: MutableLiveData<Conversation?> = MutableLiveData() val conversation: MutableLiveData<Conversation?> = MutableLiveData()
var initConversation: Conversation? = null var initConversation: Conversation? = null
val messagesLiveData = Transformations.switchMap(conversation) { val messagesLiveData = Transformations.switchMap(conversation) {
@ -54,10 +55,10 @@ class ChatViewModel constructor(application: Application,
var conversationPassword: String? = null var conversationPassword: String? = null
fun init(user: UserNgEntity, conversationToken: String, conversationPassword: String?) { fun init(user: User, conversationToken: String, conversationPassword: String?) {
viewModelScope.launch { viewModelScope.launch {
this@ChatViewModel.user = user this@ChatViewModel.user = user
this@ChatViewModel.initConversation = conversationsRepository.getConversationForUserWithToken(user.id, conversationToken) this@ChatViewModel.initConversation = conversationsRepository.getConversationForUserWithToken(user.id!!, conversationToken)
this@ChatViewModel.conversationPassword = conversationPassword this@ChatViewModel.conversationPassword = conversationPassword
globalService.getConversation(conversationToken, this@ChatViewModel) 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) { override suspend fun gotConversationInfoForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) {
if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) { if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) {
if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) { 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 -> conversation.token?.let { conversationToken ->
globalService.joinConversation(conversationToken, conversationPassword, this) globalService.joinConversation(conversationToken, conversationPassword, this)
} }

View File

@ -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<T : ChatElement>(loadingIndicatorsEnabled: Boolean = true, errorIndicatorEnabled: Boolean = false, emptyIndicatorEnabled: Boolean = false) : MainSource<T>(loadingIndicatorsEnabled, errorIndicatorEnabled, emptyIndicatorEnabled) {
override fun areItemsTheSame(first: T, second: T): Boolean {
TODO("Not yet implemented")
}
}

View File

@ -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<String, String>? = null)
}

View File

@ -38,9 +38,9 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.controllers.ChatController
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.newarch.data.presenters.AdvancedEmptyPresenter 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.ContactsViewOperationState
import com.nextcloud.talk.newarch.features.contactsflow.ParticipantElement import com.nextcloud.talk.newarch.features.contactsflow.ParticipantElement
import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView
@ -180,7 +180,7 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
val bundle = Bundle() val bundle = Bundle()
if (!hasToken || isNewGroupConversation) { if (!hasToken || isNewGroupConversation) {
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken) bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
router.replaceTopController(RouterTransaction.with(ChatController(bundle)) router.replaceTopController(RouterTransaction.with(ChatView(bundle))
.popChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler())
.pushChangeHandler(HorizontalChangeHandler())) .pushChangeHandler(HorizontalChangeHandler()))
} else { } else {

View File

@ -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.data.presenters.AdvancedEmptyPresenter
import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView
import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher 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.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.utils.ConductorRemapping 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( items.add(
BasicListItemWithImage( BasicListItemWithImage(
drawable.ic_exit_to_app_black_24dp, context.getString 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( items.add(
BasicListItemWithImage( BasicListItemWithImage(
drawable.ic_delete_grey600_24dp, context.getString( drawable.ic_delete_grey600_24dp, context.getString(

View File

@ -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.push.PushConfiguration
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
import com.nextcloud.talk.newarch.local.models.other.UserStatus import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.ApiUtils
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -24,6 +25,16 @@ data class User(
var status: UserStatus? = null var status: UserStatus? = null
) : Parcelable ) : 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 { fun User.toUserEntity(): UserNgEntity {
var userNgEntity: UserNgEntity? = null var userNgEntity: UserNgEntity? = null
this.id?.let { this.id?.let {

View File

@ -91,11 +91,3 @@ fun UserNgEntity.toUser(): User {
} }
fun UserNgEntity.getCredentials(): String = ApiUtils.getCredentials(username, token) 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
}

View File

@ -157,7 +157,7 @@ class CallService : Service(), KoinComponent, CoroutineScope {
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString())) .setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
.setAutoCancel(true) .setAutoCancel(true)
.setOngoing(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) .setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true) .setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences), AudioManager.STREAM_RING) .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 imageLoader = networkComponents.getImageLoader(signatureVerification.userEntity!!.toUser())
val request = Images().getRequestForUrl( val request = Images().getRequestForUrl(
imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity, imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity!!.toUser(),
target, null, CircleCropTransformation()) target, null, CircleCropTransformation())
imageLoader.load(request) imageLoader.load(request)

View File

@ -36,6 +36,7 @@ import coil.target.Target
import coil.transform.Transformation import coil.transform.Transformation
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.models.json.conversations.Conversation 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.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
@ -46,7 +47,7 @@ class Images {
context: Context, context: Context,
url: String, url: String,
userEntity: userEntity:
UserNgEntity?, User?,
target: Target?, target: Target?,
lifecycleOwner: LifecycleOwner?, lifecycleOwner: LifecycleOwner?,
vararg transformations: Transformation vararg transformations: Transformation

View File

@ -24,7 +24,7 @@ import android.os.Bundle
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.newarch.features.chat.ChatView
object ConductorRemapping { object ConductorRemapping {
fun remapChatController( fun remapChatController(
@ -51,13 +51,13 @@ object ConductorRemapping {
} else { } else {
if (!replaceTop) { if (!replaceTop) {
router.pushController( router.pushController(
RouterTransaction.with(ChatController(bundle)) RouterTransaction.with(ChatView(bundle))
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag) .popChangeHandler(HorizontalChangeHandler()).tag(tag)
) )
} else { } else {
router.replaceTopController( router.replaceTopController(
RouterTransaction.with(ChatController(bundle)) RouterTransaction.with(ChatView(bundle))
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag) .popChangeHandler(HorizontalChangeHandler()).tag(tag)
) )

View File

@ -61,6 +61,7 @@ import com.google.android.material.chip.ChipDrawable
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.events.UserMentionClickEvent 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.local.models.UserNgEntity
import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.newarch.utils.Images
import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.utils.text.Spans
@ -186,7 +187,7 @@ object DisplayUtils {
context: Context, context: Context,
id: String, id: String,
label: CharSequence, label: CharSequence,
conversationUser: UserNgEntity, conversationUser: User,
type: String, type: String,
@XmlRes chipResource: Int, @XmlRes chipResource: Int,
emojiEditText: EditText? emojiEditText: EditText?
@ -264,7 +265,7 @@ object DisplayUtils {
id: String, id: String,
label: String, label: String,
type: String, type: String,
conversationUser: UserNgEntity, conversationUser: User,
@XmlRes chipXmlRes: Int @XmlRes chipXmlRes: Int
): Spannable { ): Spannable {

View File

@ -36,6 +36,7 @@ object BundleKeys {
val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD" val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD"
val KEY_CONVERSATION_TOKEN = "KEY_CONVERSATION_TOKEN" val KEY_CONVERSATION_TOKEN = "KEY_CONVERSATION_TOKEN"
val KEY_USER_ENTITY = "KEY_USER_ENTITY" val KEY_USER_ENTITY = "KEY_USER_ENTITY"
val KEY_USER = "KEY_USER"
val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION" val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION"
val KEY_NEW_GROUP_CONVERSATION = "KEY_NEW_GROUP_CONVERSATION" val KEY_NEW_GROUP_CONVERSATION = "KEY_NEW_GROUP_CONVERSATION"
val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS" val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS"

View File

@ -72,6 +72,14 @@
app:inputTextSize="16sp" app:inputTextSize="16sp"
app:showAttachmentButton="true" /> app:showAttachmentButton="true" />
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/messageInputView"
app:stackFromEnd="true"
app:reverseLayout="true"
android:id="@+id/messagesRecyclerView"/>
<com.stfalcon.chatkit.messages.MessagesList <com.stfalcon.chatkit.messages.MessagesList
android:id="@+id/messagesListView" android:id="@+id/messagesListView"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,89 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="2dp">
<com.google.android.flexbox.FlexboxLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/messageUserAvatar"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<ImageView
android:id="@id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
tools:src="@tools:sample/backgrounds/scenic"/>
<androidx.emoji.widget.EmojiTextView
android:id="@id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:textColor="@color/warm_grey_four"
android:textColorLink="@color/warm_grey_four"
android:textIsSelectable="false"
android:textSize="12sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
<TextView
android:id="@id/messageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:textColor="@color/warm_grey_four"
app:layout_alignSelf="center" />
</com.google.android.flexbox.FlexboxLayout>
<ImageView
android:id="@id/messageUserAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
/>
</RelativeLayout>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="2dp">
<com.google.android.flexbox.FlexboxLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/messageUserAvatar"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<ImageView
android:id="@+id/incomingPreviewImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
app:layout_alignSelf="flex_start"
tools:src="@tools:sample/backgrounds/scenic"/>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/incomingPreviewMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:textColor="@color/warm_grey_four"
android:textColorLink="@color/warm_grey_four"
android:textIsSelectable="false"
android:textSize="12sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
<TextView
android:id="@+id/incomingPreviewTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:textColor="@color/warm_grey_four"
app:layout_alignSelf="center" />
</com.google.android.flexbox.FlexboxLayout>
<ImageView
android:id="@id/messageUserAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
/>
</RelativeLayout>

View File

@ -57,7 +57,7 @@
<include layout="@layout/item_message_quote" android:visibility="gone"/> <include layout="@layout/item_message_quote" android:visibility="gone"/>
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@id/messageText" android:id="@+id/incomingMessageText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
@ -68,10 +68,10 @@
app:layout_wrapBefore="true" /> app:layout_wrapBefore="true" />
<TextView <TextView
android:id="@id/messageTime" android:id="@+id/incomingMessageTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/messageText" android:layout_below="@+id/outgoingMessageText"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_alignSelf="center" /> app:layout_alignSelf="center" />

View File

@ -41,7 +41,7 @@
app:justifyContent="flex_end"> app:justifyContent="flex_end">
<ImageView <ImageView
android:id="@id/image" android:id="@+id/outgoingPreviewImage"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
@ -52,7 +52,7 @@
tools:src="@tools:sample/backgrounds/scenic"/> tools:src="@tools:sample/backgrounds/scenic"/>
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@id/messageText" android:id="@+id/outgoingPreviewMessageText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autoLink="all" android:autoLink="all"
@ -65,7 +65,7 @@
app:layout_wrapBefore="true" /> app:layout_wrapBefore="true" />
<TextView <TextView
android:id="@id/messageTime" android:id="@+id/outgoingPreviewTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"

View File

@ -41,7 +41,7 @@
<include layout="@layout/item_message_quote" android:visibility="gone"/> <include layout="@layout/item_message_quote" android:visibility="gone"/>
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@id/messageText" android:id="@+id/outgoingMessageText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true" android:layout_alignWithParentIfMissing="true"
@ -54,10 +54,10 @@
android:autoLink="all"/> android:autoLink="all"/>
<TextView <TextView
android:id="@id/messageTime" android:id="@+id/outgoingMessageTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/messageText" android:layout_below="@id/outgoingMessageText"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_alignSelf="center" /> app:layout_alignSelf="center" />

View File

@ -40,7 +40,7 @@
app:justifyContent="flex_end"> app:justifyContent="flex_end">
<androidx.emoji.widget.EmojiTextView <androidx.emoji.widget.EmojiTextView
android:id="@+id/messageText" android:id="@+id/systemMessageText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
@ -54,7 +54,7 @@
app:layout_wrapBefore="true"/> app:layout_wrapBefore="true"/>
<TextView <TextView
android:id="@id/messageTime" android:id="@+id/systemItemTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/warm_grey_four" android:textColor="@color/warm_grey_four"

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<TextView
android:id="@+id/noticeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="16dp"/>
</RelativeLayout>

View File

@ -313,5 +313,5 @@
<string name="nc_search_for_more">Vyhledat další účastníky</string> <string name="nc_search_for_more">Vyhledat další účastníky</string>
<string name="nc_new_group">Nová skupina</string> <string name="nc_new_group">Nová skupina</string>
<string name="nc_search_empty_contacts">Kam se všichni schovali?</string> <string name="nc_search_empty_contacts">Kam se všichni schovali?</string>
<string name="reject_call">Odmítnout</string> <string name="nc_reject_call">Odmítnout</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@ Meeting ist für %1$s geplant.</string>
<string name="nc_search_for_more">Weitere Teilnehmer suchen</string> <string name="nc_search_for_more">Weitere Teilnehmer suchen</string>
<string name="nc_new_group">Neue Gruppe</string> <string name="nc_new_group">Neue Gruppe</string>
<string name="nc_search_empty_contacts">Wo haben sie sich alle versteckt?</string> <string name="nc_search_empty_contacts">Wo haben sie sich alle versteckt?</string>
<string name="reject_call">Ablehnen</string> <string name="nc_reject_call">Ablehnen</string>
</resources> </resources>

View File

@ -318,5 +318,5 @@
<string name="nc_search_for_more">Αναζήτηση περισσότερων συμμετεχόντων</string> <string name="nc_search_for_more">Αναζήτηση περισσότερων συμμετεχόντων</string>
<string name="nc_new_group">Νέα ομάδα</string> <string name="nc_new_group">Νέα ομάδα</string>
<string name="nc_search_empty_contacts">Πού κρύβονταν όλοι;</string> <string name="nc_search_empty_contacts">Πού κρύβονταν όλοι;</string>
<string name="reject_call">Απόρριψη</string> <string name="nc_reject_call">Απόρριψη</string>
</resources> </resources>

View File

@ -316,5 +316,5 @@
<string name="nc_search_for_more">Buscar más participantes</string> <string name="nc_search_for_more">Buscar más participantes</string>
<string name="nc_new_group">Nuevo grupo</string> <string name="nc_new_group">Nuevo grupo</string>
<string name="nc_search_empty_contacts">¿Dónde se escondieron todos?</string> <string name="nc_search_empty_contacts">¿Dónde se escondieron todos?</string>
<string name="reject_call">Rechazar</string> <string name="nc_reject_call">Rechazar</string>
</resources> </resources>

View File

@ -321,5 +321,5 @@
<string name="nc_search_for_more">Bilatu parte-hartzaile gehiago</string> <string name="nc_search_for_more">Bilatu parte-hartzaile gehiago</string>
<string name="nc_new_group">Talde berria</string> <string name="nc_new_group">Talde berria</string>
<string name="nc_search_empty_contacts">Non ezkutatu dira denak?</string> <string name="nc_search_empty_contacts">Non ezkutatu dira denak?</string>
<string name="reject_call">Baztertu</string> <string name="nc_reject_call">Baztertu</string>
</resources> </resources>

View File

@ -270,5 +270,5 @@
<string name="nc_search_for_more">Etsi lisää osallistujia</string> <string name="nc_search_for_more">Etsi lisää osallistujia</string>
<string name="nc_new_group">Uusi ryhmä</string> <string name="nc_new_group">Uusi ryhmä</string>
<string name="nc_search_empty_contacts">Mihin he kaikki piiloutuivat?</string> <string name="nc_search_empty_contacts">Mihin he kaikki piiloutuivat?</string>
<string name="reject_call">Hylkää</string> <string name="nc_reject_call">Hylkää</string>
</resources> </resources>

View File

@ -316,5 +316,5 @@ Le démarrage de cette réunion est prévu à %1$s.</string>
<string name="nc_search_for_more">Rechercher d\'autres participants</string> <string name="nc_search_for_more">Rechercher d\'autres participants</string>
<string name="nc_new_group">Nouveau groupe</string> <string name="nc_new_group">Nouveau groupe</string>
<string name="nc_search_empty_contacts">Où se cachent-ils tous ?</string> <string name="nc_search_empty_contacts">Où se cachent-ils tous ?</string>
<string name="reject_call">Refuser</string> <string name="nc_reject_call">Refuser</string>
</resources> </resources>

View File

@ -322,5 +322,5 @@ móbiles. Pode tentar unirse á chamada empregando o navegador web.</string>
<string name="nc_search_for_more">Buscar máis participantes</string> <string name="nc_search_for_more">Buscar máis participantes</string>
<string name="nc_new_group">Grupo novo</string> <string name="nc_new_group">Grupo novo</string>
<string name="nc_search_empty_contacts">Onde se agocharon todos?</string> <string name="nc_search_empty_contacts">Onde se agocharon todos?</string>
<string name="reject_call">Rexeitar</string> <string name="nc_reject_call">Rexeitar</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Cerca altri partecipanti</string> <string name="nc_search_for_more">Cerca altri partecipanti</string>
<string name="nc_new_group">Nuovo gruppo</string> <string name="nc_new_group">Nuovo gruppo</string>
<string name="nc_search_empty_contacts">Dove si sono nascosti?</string> <string name="nc_search_empty_contacts">Dove si sono nascosti?</string>
<string name="reject_call">Rifiuta</string> <string name="nc_reject_call">Rifiuta</string>
</resources> </resources>

View File

@ -319,5 +319,5 @@ Je kunt proberen om aan het gesprek deel te nemen via een browser.</string>
<string name="nc_search_for_more">Zoek meer deelnemers</string> <string name="nc_search_for_more">Zoek meer deelnemers</string>
<string name="nc_new_group">Nieuwe groep</string> <string name="nc_new_group">Nieuwe groep</string>
<string name="nc_search_empty_contacts">Waar zijn ze allemaal naar toe?</string> <string name="nc_search_empty_contacts">Waar zijn ze allemaal naar toe?</string>
<string name="reject_call">Afwijzen</string> <string name="nc_reject_call">Afwijzen</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Wyszukaj więcej uczestników</string> <string name="nc_search_for_more">Wyszukaj więcej uczestników</string>
<string name="nc_new_group">Nowa grupa</string> <string name="nc_new_group">Nowa grupa</string>
<string name="nc_search_empty_contacts">Gdzie oni wszyscy się schowali?</string> <string name="nc_search_empty_contacts">Gdzie oni wszyscy się schowali?</string>
<string name="reject_call">Odrzuć </string> <string name="nc_reject_call">Odrzuć </string>
</resources> </resources>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Procurar por mais participantes</string> <string name="nc_search_for_more">Procurar por mais participantes</string>
<string name="nc_new_group">Novo grupo</string> <string name="nc_new_group">Novo grupo</string>
<string name="nc_search_empty_contacts">Onde eles todos se esconderam?</string> <string name="nc_search_empty_contacts">Onde eles todos se esconderam?</string>
<string name="reject_call">Rejeitar</string> <string name="nc_reject_call">Rejeitar</string>
</resources> </resources>

View File

@ -315,5 +315,5 @@
<string name="nc_search_for_more">Искать дополнительных участников</string> <string name="nc_search_for_more">Искать дополнительных участников</string>
<string name="nc_new_group">Новая группа</string> <string name="nc_new_group">Новая группа</string>
<string name="nc_search_empty_contacts">Где все?</string> <string name="nc_search_empty_contacts">Где все?</string>
<string name="reject_call">Отклонить</string> <string name="nc_reject_call">Отклонить</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@ v zozname rozhovorov</string>
<string name="nc_search_for_more">Vyhľadanie ďalších účastníkov</string> <string name="nc_search_for_more">Vyhľadanie ďalších účastníkov</string>
<string name="nc_new_group">Nová skupina</string> <string name="nc_new_group">Nová skupina</string>
<string name="nc_search_empty_contacts">Kde sa všetci schovali?</string> <string name="nc_search_empty_contacts">Kde sa všetci schovali?</string>
<string name="reject_call">Odmietnuť </string> <string name="nc_reject_call">Odmietnuť </string>
</resources> </resources>

View File

@ -320,5 +320,5 @@ Pozdravite prijatelje in znance.</string>
<string name="nc_search_for_more">Poišči več udeležencev</string> <string name="nc_search_for_more">Poišči več udeležencev</string>
<string name="nc_new_group">Nova skupina</string> <string name="nc_new_group">Nova skupina</string>
<string name="nc_search_empty_contacts">Kam so se vsi skrili?</string> <string name="nc_search_empty_contacts">Kam so se vsi skrili?</string>
<string name="reject_call">Zavrni</string> <string name="nc_reject_call">Zavrni</string>
</resources> </resources>

View File

@ -319,5 +319,5 @@
<string name="nc_search_for_more">Тражи још учесника</string> <string name="nc_search_for_more">Тражи још учесника</string>
<string name="nc_new_group">Нова група</string> <string name="nc_new_group">Нова група</string>
<string name="nc_search_empty_contacts">Где су сви нестали?</string> <string name="nc_search_empty_contacts">Где су сви нестали?</string>
<string name="reject_call">Одбиј</string> <string name="nc_reject_call">Одбиј</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Sök efter fler deltagare</string> <string name="nc_search_for_more">Sök efter fler deltagare</string>
<string name="nc_new_group">Ny grupp</string> <string name="nc_new_group">Ny grupp</string>
<string name="nc_search_empty_contacts">Var gömmer sig alla?</string> <string name="nc_search_empty_contacts">Var gömmer sig alla?</string>
<string name="reject_call">Avvisa</string> <string name="nc_reject_call">Avvisa</string>
</resources> </resources>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Başka katılımcılar arayın</string> <string name="nc_search_for_more">Başka katılımcılar arayın</string>
<string name="nc_new_group">Yeni grup</string> <string name="nc_new_group">Yeni grup</string>
<string name="nc_search_empty_contacts">Hepsi nereye gizlendiler?</string> <string name="nc_search_empty_contacts">Hepsi nereye gizlendiler?</string>
<string name="reject_call">Reddet</string> <string name="nc_reject_call">Reddet</string>
</resources> </resources>

View File

@ -316,5 +316,5 @@
<string name="nc_search_for_more">搜索更多参与者</string> <string name="nc_search_for_more">搜索更多参与者</string>
<string name="nc_new_group">新建群组</string> <string name="nc_new_group">新建群组</string>
<string name="nc_search_empty_contacts">他们都藏在哪里?</string> <string name="nc_search_empty_contacts">他们都藏在哪里?</string>
<string name="reject_call">拒绝</string> <string name="nc_reject_call">拒绝</string>
</resources> </resources>

View File

@ -344,5 +344,5 @@
<string name="nc_search_for_more">Search for more participants</string> <string name="nc_search_for_more">Search for more participants</string>
<string name="nc_new_group">New group</string> <string name="nc_new_group">New group</string>
<string name="nc_search_empty_contacts">Where did they all hide?</string> <string name="nc_search_empty_contacts">Where did they all hide?</string>
<string name="reject_call">Reject </string> <string name="nc_reject_call">Reject </string>
</resources> </resources>