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 {
work_version = '2.3.3'
koin_version = "2.1.0-alpha-1"
koin_version = "2.1.4"
lifecycle_version = '2.2.0'
coil_version = "0.9.5"
room_version = "2.2.4"

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
//region Protected methods
protected fun startKoin() {
private fun startKoin() {
startKoin {
androidContext(this@NextcloudTalkApplication)
androidLogger()

View File

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

View File

@ -32,6 +32,7 @@ import coil.api.load
import com.nextcloud.talk.R
import com.nextcloud.talk.components.filebrowser.models.BrowserFile
import com.nextcloud.talk.interfaces.SelectionInterface
import com.nextcloud.talk.newarch.local.models.User
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.ApiUtils
@ -47,7 +48,7 @@ import org.koin.core.inject
class BrowserFileItem(
val model: BrowserFile,
private val activeUser: UserNgEntity,
private val activeUser: User,
private val selectionInterface: SelectionInterface
) : AbstractFlexibleItem<BrowserFileItem.ViewHolder>(), IFilterable<String>, KoinComponent {
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.interfaces.SelectionInterface
import com.nextcloud.talk.jobs.ShareOperationWorker
import com.nextcloud.talk.newarch.local.models.User
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.bundle.BundleKeys
import eu.davidea.fastscroller.FastScroller
@ -83,13 +84,13 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex
private var listingAbstractClass: ListingAbstractClass? = null
private val browserType: BrowserType
private var currentPath: String? = null
private val activeUser: UserNgEntity
private val activeUser: User
private val roomToken: String?
init {
setHasOptionsMenu(true)
browserType = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_BROWSER_TYPE))
activeUser = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_USER_ENTITY))
activeUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY)!!
roomToken = args.getString(BundleKeys.KEY_CONVERSATION_TOKEN)
currentPath = "/"
@ -130,7 +131,7 @@ class BrowserController(args: Bundle) : BaseController(), ListingInterface, Flex
iterator.remove()
if (paths.size == 10 || !iterator.hasNext()) {
data = Data.Builder()
.putLong(BundleKeys.KEY_INTERNAL_USER_ID, activeUser.id)
.putLong(BundleKeys.KEY_INTERNAL_USER_ID, activeUser.id!!)
.putString(BundleKeys.KEY_CONVERSATION_TOKEN, roomToken)
.putStringArray(BundleKeys.KEY_FILE_PATHS, paths.toTypedArray())
.build()

View File

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

View File

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

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

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

View File

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

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

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
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.InputFilter
import android.text.TextUtils
import android.text.TextWatcher
import android.view.*
import android.widget.AbsListView
import android.widget.ImageView
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import coil.api.load
import coil.target.Target
import coil.transform.CircleCropTransformation
@ -43,61 +46,61 @@ import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.messages.*
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
import com.nextcloud.talk.controllers.ChatController
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.features.chat.interfaces.ImageLoaderInterface
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.local.models.getMaxMessageLength
import com.nextcloud.talk.newarch.mvvm.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.newarch.utils.Images
import com.nextcloud.talk.newarch.utils.NetworkComponents
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.utils.*
import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage
import com.stfalcon.chatkit.messages.MessageHolders
import com.otaliastudios.elements.Adapter
import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Presenter
import com.otaliastudios.elements.pagers.PageSizePager
import com.stfalcon.chatkit.messages.MessagesListAdapter
import com.stfalcon.chatkit.utils.DateFormatter
import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.controller_chat.view.*
import kotlinx.android.synthetic.main.conversations_list_view.view.*
import kotlinx.android.synthetic.main.lobby_view.view.*
import kotlinx.android.synthetic.main.view_message_input.view.*
import org.koin.android.ext.android.inject
import org.parceler.Parcels
import java.util.*
import coil.ImageLoader as CoilImageLoader
import com.stfalcon.chatkit.commons.ImageLoader as ChatKitImageLoader
class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter
.OnMessageLongClickListener<IMessage>, MessagesListAdapter.Formatter<Date> {
class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this)
override val lifecycleOwner = ControllerLifecycleOwner(this)
lateinit var viewModel: ChatViewModel
private lateinit var viewModel: ChatViewModel
val factory: ChatViewModelFactory by inject()
val imageLoader: CoilImageLoader by inject()
private val networkComponents: NetworkComponents by inject()
var conversationInfoMenuItem: MenuItem? = null
var conversationVoiceCallMenuItem: MenuItem? = null
var conversationVideoMenuItem: MenuItem? = null
private var newMessagesCount = 0
private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage>
private lateinit var mentionAutocomplete: Autocomplete<*>
private var shouldShowLobby: Boolean = false
private var isReadOnlyConversation: Boolean = false
private lateinit var messagesAdapter: Adapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup
@ -105,12 +108,22 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
setHasOptionsMenu(true)
actionBar?.show()
viewModel = viewModelProvider(factory).get(ChatViewModel::class.java)
viewModel.init(args.getParcelable(BundleKeys.KEY_USER_ENTITY)!!, args.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, args.getString(KEY_CONVERSATION_PASSWORD))
val view = super.onCreateView(inflater, container)
viewModel.init(bundle.getParcelable(BundleKeys.KEY_USER)!!, bundle.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, bundle.getString(KEY_CONVERSATION_PASSWORD))
messagesAdapter = Adapter.builder(this)
.setPager(PageSizePager(80))
//.addSource(ChatViewSource(itemsPerPage = 10))
.addSource(ChatDateHeaderSource(activity as Context, ChatElementTypes.DATE_HEADER.ordinal))
.addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state))
.addPresenter(ChatPresenter(activity as Context, ::onElementClick, ::onElementLongClick, this))
.setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true)
.into(view.messagesRecyclerView)
viewModel.apply {
conversation.observe(this@ChatView) { conversation ->
setTitle()
setupAdapter()
if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == conversation?.type) {
loadAvatar()
@ -124,36 +137,100 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
activity?.invalidateOptionsMenu()
if (shouldShowLobby) {
view?.messagesListView?.visibility = View.GONE
view?.messageInputView?.visibility = View.GONE
view?.lobbyView?.visibility = View.VISIBLE
view.messagesListView?.visibility = View.GONE
view.messageInputView?.visibility = View.GONE
view.lobbyView?.visibility = View.VISIBLE
val timer = conversation.lobbyTimer
if (timer != null && timer != 0L) {
view?.lobbyTextView?.text = String.format(
val unit = if (timer != null && timer != 0L) {
view.lobbyTextView?.text = String.format(
resources!!.getString(R.string.nc_lobby_waiting_with_date),
DateUtils.getLocalDateStringFromTimestampForLobby(
conversation.lobbyTimer!!
))
} else {
view?.lobbyTextView?.setText(R.string.nc_lobby_waiting)
view.lobbyTextView?.setText(R.string.nc_lobby_waiting)
}
} else {
view?.messagesListView?.visibility = View.GONE
view?.lobbyView?.visibility = View.GONE
view.messagesListView?.visibility = View.GONE
view.lobbyView?.visibility = View.GONE
if (isReadOnlyConversation) {
view?.messageInputView?.visibility = View.GONE
view.messageInputView?.visibility = View.GONE
} else {
view?.messageInputView?.visibility = View.VISIBLE
view.messageInputView?.visibility = View.VISIBLE
}
}
}
}
return super.onCreateView(inflater, container)
return view
}
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<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) {
super.onAttach(view)
@ -185,56 +262,15 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
private fun setupViews() {
view?.let { view ->
view.recyclerView.initRecyclerView(
view.messagesRecyclerView.initRecyclerView(
LinearLayoutManager(view.context), recyclerViewAdapter, false
)
recyclerViewAdapter.setLoadMoreListener(this)
recyclerViewAdapter.setDateHeadersFormatter { format(it) }
recyclerViewAdapter.setOnMessageLongClickListener { onMessageLongClick(it) }
view.popupBubbleView.setRecyclerView(view.messagesListView)
view.popupBubbleView.setPopupBubbleListener { context ->
if (newMessagesCount != 0) {
val scrollPosition: Int
if (newMessagesCount - 1 < 0) {
scrollPosition = 0
} else {
scrollPosition = newMessagesCount - 1
}
view.messagesListView.postDelayed({
view.messagesListView.smoothScrollToPosition(scrollPosition)
}, 200)
}
}
view.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
if (newMessagesCount != 0) {
val layoutManager: LinearLayoutManager = view.messagesListView.layoutManager as LinearLayoutManager
if (layoutManager.findFirstCompletelyVisibleItemPosition() <
newMessagesCount
) {
newMessagesCount = 0
view.popupBubbleView?.hide()
}
}
}
}
})
val filters = arrayOfNulls<InputFilter>(1)
val lengthFilter = viewModel.user.getMaxMessageLength()
filters[0] = InputFilter.LengthFilter(lengthFilter)
view.messageInput.filters = filters
@ -365,9 +401,9 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
viewModel.conversation.value?.let {
val bundle = Bundle()
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)
router.pushController(
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() {
val imageLoader = networkComponents.getImageLoader(viewModel.user)
val avatarSize = DisplayUtils.convertDpToPixel(
conversationVoiceCallMenuItem?.icon!!
.intrinsicWidth.toFloat(), activity!!
@ -459,9 +438,7 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
), viewModel.user, target, this,
CircleCropTransformation()
)
imageLoader.load(avatarRequest)
}
}
}
@ -474,35 +451,30 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
return viewModel.conversation.value?.displayName
}
override fun hasContentFor(message: IMessage, type: Byte): Boolean {
when (type) {
ChatController.CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage)
ChatController.CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1"
}
return false
override fun getImageLoader(): ImageLoader {
return networkComponents.getImageLoader(viewModel.user)
}
override fun format(date: Date): String {
return when {
DateFormatter.isToday(date) -> {
resources!!.getString(R.string.nc_date_header_today)
override fun loadImage(imageView: ImageView, url: String, payload: MutableMap<String, String>?) {
val imageLoader = networkComponents.getImageLoader(viewModel.user)
imageLoader.load(activity as Context, url) {
if (url.contains("/avatar/")) {
transformations(CircleCropTransformation())
} else {
payload?.let {
if (payload.containsKey("mimetype")) {
placeholder(DrawableUtils.getDrawableResourceIdForMimeType(payload["mimetype"])
)
}
}
}
DateFormatter.isYesterday(date) -> {
resources!!.getString(R.string.nc_date_header_yesterday)
}
else -> {
DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
target(imageView)
val needsAuthBasedOnUrl = url.contains("index.php/core/preview?fileId=") || url.contains("index.php/avatar/")
if (url.startsWith(viewModel.user.baseUrl) && needsAuthBasedOnUrl) {
addHeader("Authorization", viewModel.user.getCredentials())
}
}
}
override fun onLoadMore(page: Int, totalItemsCount: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onMessageLongClick(message: IMessage?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

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

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

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.features.contactsflow.contacts.ContactsView
import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher
import com.nextcloud.talk.newarch.local.models.toUser
import com.nextcloud.talk.newarch.mvvm.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.utils.ConductorRemapping
@ -265,7 +266,7 @@ class ConversationsListView : BaseView() {
)
}
if (conversation.canLeave(viewModel.globalService.currentUserLiveData.value!!)) {
if (conversation.canLeave(viewModel.globalService.currentUserLiveData.value!!.toUser())) {
items.add(
BasicListItemWithImage(
drawable.ic_exit_to_app_black_24dp, context.getString
@ -274,7 +275,7 @@ class ConversationsListView : BaseView() {
)
}
if (conversation.canModerate(viewModel.globalService.currentUserLiveData.value!!)) {
if (conversation.canModerate(viewModel.globalService.currentUserLiveData.value!!.toUser())) {
items.add(
BasicListItemWithImage(
drawable.ic_delete_grey600_24dp, context.getString(

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.signaling.settings.SignalingSettings
import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.ApiUtils
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.Serializable
@ -24,6 +25,16 @@ data class User(
var status: UserStatus? = null
) : Parcelable
fun User.getMaxMessageLength(): Int {
return capabilities?.spreedCapability?.config?.get("chat")?.get("max-length")?.toInt() ?: 1000
}
fun User.getCredentials(): String = ApiUtils.getCredentials(username, token)
fun User.hasSpreedFeatureCapability(capabilityName: String): Boolean {
return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false
}
fun User.toUserEntity(): UserNgEntity {
var userNgEntity: UserNgEntity? = null
this.id?.let {

View File

@ -91,11 +91,3 @@ fun UserNgEntity.toUser(): User {
}
fun UserNgEntity.getCredentials(): String = ApiUtils.getCredentials(username, token)
fun UserNgEntity.hasSpreedFeatureCapability(capabilityName: String): Boolean {
return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false
}
fun UserNgEntity.getMaxMessageLength(): Int {
return capabilities?.spreedCapability?.config?.get("chat")?.get("max-length")?.toInt() ?: 1000
}

View File

@ -157,7 +157,7 @@ class CallService : Service(), KoinComponent, CoroutineScope {
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.subject.toString()))
.setAutoCancel(true)
.setOngoing(true)
.addAction(R.drawable.ic_call_end_white_24px, resources.getString(R.string.reject_call), rejectCallPendingIntent)
.addAction(R.drawable.ic_call_end_white_24px, resources.getString(R.string.nc_reject_call), rejectCallPendingIntent)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(NotificationUtils.getCallSoundUri(applicationContext, appPreferences), AudioManager.STREAM_RING)
@ -184,7 +184,7 @@ class CallService : Service(), KoinComponent, CoroutineScope {
val imageLoader = networkComponents.getImageLoader(signatureVerification.userEntity!!.toUser())
val request = Images().getRequestForUrl(
imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity,
imageLoader, applicationContext, avatarUrl, signatureVerification.userEntity!!.toUser(),
target, null, CircleCropTransformation())
imageLoader.load(request)

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,14 @@
app:inputTextSize="16sp"
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
android:id="@+id/messagesListView"
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"/>
<androidx.emoji.widget.EmojiTextView
android:id="@id/messageText"
android:id="@+id/incomingMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
@ -68,10 +68,10 @@
app:layout_wrapBefore="true" />
<TextView
android:id="@id/messageTime"
android:id="@+id/incomingMessageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/messageText"
android:layout_below="@+id/outgoingMessageText"
android:layout_marginStart="8dp"
app:layout_alignSelf="center" />

View File

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

View File

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

View File

@ -40,7 +40,7 @@
app:justifyContent="flex_end">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/messageText"
android:id="@+id/systemMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
@ -54,7 +54,7 @@
app:layout_wrapBefore="true"/>
<TextView
android:id="@id/messageTime"
android:id="@+id/systemItemTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_new_group">Nová skupina</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>

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_new_group">Neue Gruppe</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>

View File

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

View File

@ -316,5 +316,5 @@
<string name="nc_search_for_more">Buscar más participantes</string>
<string name="nc_new_group">Nuevo grupo</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>

View File

@ -321,5 +321,5 @@
<string name="nc_search_for_more">Bilatu parte-hartzaile gehiago</string>
<string name="nc_new_group">Talde berria</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>

View File

@ -270,5 +270,5 @@
<string name="nc_search_for_more">Etsi lisää osallistujia</string>
<string name="nc_new_group">Uusi ryhmä</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>

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_new_group">Nouveau groupe</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>

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_new_group">Grupo novo</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>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Cerca altri partecipanti</string>
<string name="nc_new_group">Nuovo gruppo</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>

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_new_group">Nieuwe groep</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>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Wyszukaj więcej uczestników</string>
<string name="nc_new_group">Nowa grupa</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>

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Procurar por mais participantes</string>
<string name="nc_new_group">Novo grupo</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>

View File

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

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_new_group">Nova skupina</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>

View File

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

View File

@ -320,5 +320,5 @@
<string name="nc_search_for_more">Sök efter fler deltagare</string>
<string name="nc_new_group">Ny grupp</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>

View File

@ -320,5 +320,5 @@
<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_search_empty_contacts">Hepsi nereye gizlendiler?</string>
<string name="reject_call">Reddet</string>
<string name="nc_reject_call">Reddet</string>
</resources>

View File

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

View File

@ -344,5 +344,5 @@
<string name="nc_search_for_more">Search for more participants</string>
<string name="nc_new_group">New group</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>