Working offline chat

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-04-05 23:06:17 +02:00
parent 6ea115a4f9
commit 16102774f5
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
44 changed files with 713 additions and 987 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "4976b952409bfae25e1f9bc8df18c11c",
"identityHash": "4e8c1ae6a440d8491937afe33a3ab085",
"entities": [
{
"tableName": "conversations",
@ -212,7 +212,7 @@
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `parent` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "id",
@ -262,6 +262,18 @@
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentMessage",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyable",
"columnName": "replyable",
@ -389,7 +401,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4976b952409bfae25e1f9bc8df18c11c')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4e8c1ae6a440d8491937afe33a3ab085')"
]
}
}

View File

@ -46,6 +46,7 @@ import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryView
import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListView
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.toUser
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
@ -147,7 +148,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
// due to complications with persistablebundle not supporting complex types we do this magic
// remove this once we rewrite chat magic
val extras = intent.extras!!
extras.putParcelable(BundleKeys.KEY_USER_ENTITY, it)
extras.putParcelable(BundleKeys.KEY_USER, it.toUser())
withContext(Dispatchers.Main) {
ConductorRemapping.remapChatController(
router!!, it.id,

View File

@ -76,7 +76,6 @@ import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
import com.nextcloud.talk.utils.ui.MaterialPreferenceCategoryWithRightLink
import com.yarolegovich.lovelydialog.LovelySaveStateHandler
import com.yarolegovich.lovelydialog.LovelyStandardDialog
@ -190,7 +189,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
private var roomDisposable: Disposable? = null
private var participantsDisposable: Disposable? = null
private var databaseStorageModule: DatabaseStorageModule? = null
private var conversation: Conversation? = null
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
@ -253,15 +251,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
saveStateHandler = LovelySaveStateHandler()
}
if (databaseStorageModule == null) {
databaseStorageModule = DatabaseStorageModule(
conversationUser!!, conversationToken!!, this)
}
notificationsPreferenceScreen.setStorageModule(databaseStorageModule)
conversationInfoWebinar.setStorageModule(databaseStorageModule)
generalConversationOptions.setStorageModule(databaseStorageModule)
actionTextView.visibility = View.GONE
}
@ -337,7 +326,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
}
fun submitGuestChange() {
if (databaseStorageModule != null && conversationUser != null && conversation != null) {
if ( conversationUser != null && conversation != null) {
if ((allowGuestsAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
ncApi.makeRoomPublic(conversationUser.getCredentials(), ApiUtils.getUrlForRoomVisibility
(conversationUser.baseUrl, conversation!!.token))
@ -379,7 +368,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
}
fun submitFavoriteChange() {
if (databaseStorageModule != null && conversationUser != null && conversation != null) {
if (conversationUser != null && conversation != null) {
if ((favoriteConversationAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
ncApi.addConversationToFavorites(conversationUser.getCredentials(), ApiUtils
.getUrlForConversationFavorites(conversationUser.baseUrl, conversation!!.token))

View File

@ -23,16 +23,38 @@
package com.nextcloud.talk.newarch.data.repository.offline
import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
import com.nextcloud.talk.newarch.local.dao.MessagesDao
import com.nextcloud.talk.newarch.local.models.toChatMessage
import com.nextcloud.talk.newarch.local.models.toMessageEntity
class MessagesRepositoryImpl(val messagesDao: MessagesDao) : MessagesRepository {
class MessagesRepositoryImpl(private val messagesDao: MessagesDao) : MessagesRepository {
override fun getMessagesWithUserForConversation(
conversationId: String
): LiveData<List<ChatMessage>> {
TODO(
"not implemented"
) //To change body of created functions use File | Settings | File Templates.
return messagesDao.getMessagesWithUserForConversation(conversationId).distinctUntilChanged().map {
it.map { messageEntity ->
messageEntity.toChatMessage()
}
}
}
override fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<ChatMessage>> {
return messagesDao.getMessagesWithUserForConversationSince(conversationId, messageId).distinctUntilChanged().map {
it.map { messageEntity ->
messageEntity.toChatMessage()
}
}
}
override suspend fun saveMessagesForConversation(messages: List<ChatMessage>): List<Long> {
val updatedMessages = messages.map {
it.toMessageEntity()
}
return messagesDao.saveMessages(*updatedMessages.toTypedArray())
}
}

View File

@ -23,6 +23,7 @@
package com.nextcloud.talk.newarch.data.repository.online
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
@ -35,9 +36,11 @@ import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOveral
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiService
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
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
import retrofit2.Response
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
override suspend fun deleteConversationForUser(
@ -95,6 +98,17 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou
}
}
override suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int): Response<ChatOverall> {
val mutableMap = mutableMapOf<String, Int>()
mutableMap["lookIntoFuture"] = lookIntoFuture
mutableMap["lastKnownMessageId"] = lastKnownMessageId
mutableMap["includeLastKnown"] = includeLastKnown
mutableMap["timeout"] = 30
mutableMap["setReadMarker"] = 1
return apiService.pullChatMessages(user.getCredentials(), ApiUtils.getUrlForChat(user.baseUrl, conversationToken), mutableMap)
}
override suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall {
return apiService.getNotification(user.getCredentials(), ApiUtils.getUrlForNotificationWithId(user.baseUrl, notificationId))
}

View File

@ -24,6 +24,7 @@ package com.nextcloud.talk.newarch.data.source.remote
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.models.json.conversations.ConversationOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
@ -33,9 +34,26 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import io.reactivex.Observable
import retrofit2.Response
import retrofit2.http.*
interface ApiService {
/*
QueryMap items are as follows:
- "lookIntoFuture": int (0 or 1),
- "limit" : int, range 100-200,
- "timeout": used with look into future, 30 default, 60 at most
- "lastKnownMessageId", int, use one from X-Chat-Last-Given
- "setReadMarker", int, default 1
- "includeLastKnown", int, default 0
*/
@GET
suspend fun pullChatMessages(@Header("Authorization") authorization: String,
@Url url: String,
@QueryMap fields: Map<String, Int>): Response<ChatOverall>
@GET
suspend fun getPeersForCall(@Header("Authorization") authorization: String,
@Url url: String): ParticipantsOverall

View File

@ -30,32 +30,38 @@ import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkReposito
import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.features.chat.ChatViewModelFactory
import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.utils.NetworkComponents
import org.koin.dsl.module
val UseCasesModule = module {
single { createGetConversationUseCase(get(), get()) }
single { createGetConversationsUseCase(get(), get()) }
single { createSetConversationFavoriteValueUseCase(get(), get()) }
single { createLeaveConversationUseCase(get(), get()) }
single { createDeleteConversationUseCase(get(), get()) }
single { createJoinConversationUseCase(get(), get()) }
single { createExitConversationUseCase(get(), get()) }
single { createGetProfileUseCase(get(), get()) }
single { createGetSignalingUseCase(get(), get()) }
single { createGetCapabilitiesUseCase(get(), get()) }
single { createRegisterPushWithProxyUseCase(get(), get()) }
single { createRegisterPushWithServerUseCase(get(), get()) }
single { createUnregisterPushWithProxyUseCase(get(), get()) }
single { createUnregisterPushWithServerUseCase(get(), get()) }
single { createGetContactsUseCase(get(), get()) }
single { createCreateConversationUseCase(get(), get()) }
single { createAddParticipantToConversationUseCase(get(), get()) }
single { setConversationPasswordUseCase(get(), get()) }
factory { createGetConversationUseCase(get(), get()) }
factory { createGetConversationsUseCase(get(), get()) }
factory { createSetConversationFavoriteValueUseCase(get(), get()) }
factory { createLeaveConversationUseCase(get(), get()) }
factory { createDeleteConversationUseCase(get(), get()) }
factory { createJoinConversationUseCase(get(), get()) }
factory { createExitConversationUseCase(get(), get()) }
factory { createGetProfileUseCase(get(), get()) }
factory { createGetSignalingUseCase(get(), get()) }
factory { createGetCapabilitiesUseCase(get(), get()) }
factory { createRegisterPushWithProxyUseCase(get(), get()) }
factory { createRegisterPushWithServerUseCase(get(), get()) }
factory { createUnregisterPushWithProxyUseCase(get(), get()) }
factory { createUnregisterPushWithServerUseCase(get(), get()) }
factory { createGetContactsUseCase(get(), get()) }
factory { createCreateConversationUseCase(get(), get()) }
factory { createAddParticipantToConversationUseCase(get(), get()) }
factory { setConversationPasswordUseCase(get(), get()) }
factory { getParticipantsForCallUseCase(get(), get()) }
factory { createGetChatMessagesUseCase(get(), get()) }
factory { getNotificationUseCase(get(), get()) }
factory { createChatViewModelFactory(get(), get(), get(), get(), get(), get()) }
}
fun createGetChatMessagesUseCase(nextcloudTalkRepository: NextcloudTalkRepository, apiErrorHandler: ApiErrorHandler): GetChatMessagesUseCase {
return GetChatMessagesUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun getNotificationUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler): GetNotificationUseCase {
return GetNotificationUseCase(nextcloudTalkRepository, apiErrorHandler)
@ -181,6 +187,6 @@ fun createExitConversationUseCase(nextcloudTalkRepository: NextcloudTalkReposito
return ExitConversationUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createChatViewModelFactory(application: Application, joinConversationUseCase: JoinConversationUseCase, exitConversationUseCase: ExitConversationUseCase, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory {
return ChatViewModelFactory(application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService)
fun createChatViewModelFactory(application: Application, networkComponents: NetworkComponents, apiErrorHandler: ApiErrorHandler, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory {
return ChatViewModelFactory(application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService)
}

View File

@ -27,5 +27,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage
interface MessagesRepository {
fun getMessagesWithUserForConversation(conversationId: String): LiveData<List<ChatMessage>>
fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<ChatMessage>>
suspend fun saveMessagesForConversation(messages: List<ChatMessage>): List<Long>
}

View File

@ -23,6 +23,7 @@
package com.nextcloud.talk.newarch.domain.repository.online
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
@ -33,9 +34,12 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.local.models.User
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import retrofit2.Response
interface NextcloudTalkRepository {
suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int = 0): Response<ChatOverall>
suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall
suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall
suspend fun setPasswordForConversation(user: UserNgEntity, conversationToken: String, password: String): GenericOverall

View File

@ -0,0 +1,40 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 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.newarch.domain.usecases
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
import retrofit2.Response
class GetChatMessagesUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<Response<ChatOverall>, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): Response<ChatOverall> {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.getChatMessagesForConversation(definitionParameters[0], definitionParameters[1], definitionParameters[2], definitionParameters[3], definitionParameters[4])
}
}

View File

@ -13,7 +13,7 @@ class ChatDateHeaderSource(private val context: Context, private val elementType
// 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 dependsOn(source: Source<*>) = source is ChatViewLiveDataSource
override fun getElementType(data: Data<ChatElement, String>): Int {
return elementType

View File

@ -2,5 +2,5 @@ package com.nextcloud.talk.newarch.features.chat
data class ChatElement(
val data: Any,
val elementType: Int
val elementType: ChatElementTypes
)

View File

@ -1,11 +1,8 @@
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
DATE_HEADER,
CHAT_MESSAGE
}

View File

@ -20,32 +20,19 @@ 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_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 {
open class ChatPresenter<T : Any>(context: Context, private val onElementClickPass: ((Page, Holder, Element<T>, Map<String, String>) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element<T>, Map<String, String>) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter<T>(context), 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)
get() = listOf(ChatElementTypes.SYSTEM_MESSAGE.ordinal, ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal, ChatElementTypes.DATE_HEADER.ordinal, ChatElementTypes.CHAT_MESSAGE.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.CHAT_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_chat_item, parent, false))
}
ChatElementTypes.SYSTEM_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_chat_system_item, parent, false))
@ -60,11 +47,11 @@ open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Hold
super.onBind(page, holder, element, payloads)
holder.itemView.setOnLongClickListener {
onElementLongClick?.invoke(page, holder, element)
onElementLongClick?.invoke(page, holder, element, mapOf())
true
}
var chatElement: ChatElement?
var chatElement: ChatElement? = null
var chatMessage: ChatMessage? = null
if (element.data is ChatElement) {
@ -74,141 +61,117 @@ open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Hold
when {
chatMessage != null -> {
val elementType = chatElement!!.elementType
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 (elementType == ChatElementTypes.CHAT_MESSAGE) {
holder.itemView.authorName?.text = it.actorDisplayName
holder.itemView.messageTime?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
holder.itemView.chatMessage.text = it.text
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)
}
if (it.actorType == "bots" && it.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)
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable))
imageLoader.getImageLoader().load(loadBuilder.build())
} else if (it.actorType == "bots") {
val drawable = TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(
">",
context.resources.getColor(R.color.black)
)
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(drawable))
imageLoader.getImageLoader().load(loadBuilder.build())
} else {
// it's ChatElementTypes.SYSTEM_MESSAGE
holder.itemView.systemMessageText.text = chatMessage.text
holder.itemView.systemItemTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
imageLoader.loadImage(holder.itemView.authorAvatar, it.user.avatar)
}
it.parentMessage?.let { parentMessage ->
holder.itemView.quotedMessageLayout.isVisible = true
holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
holder.itemView.quotedPreviewImage.setOnClickListener {
onElementClickPass?.invoke(page, holder, element, mapOf("parentMessage" to "yes"))
true
}
parentMessage.imageUrl?.let { previewMessageUrl ->
if (previewMessageUrl == "no-preview") {
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
holder.itemView.quotedPreviewImage.visibility = View.VISIBLE
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(parentMessage.selectedIndividualHashMap!!["mimetype"])) {
target(holder.itemView.previewImage)
}
} else {
holder.itemView.quotedPreviewImage.visibility = View.GONE
}
} else {
holder.itemView.quotedPreviewImage.visibility = View.VISIBLE
val mutableMap = mutableMapOf<String, String>()
if (parentMessage.selectedIndividualHashMap?.containsKey("mimetype") == true) {
mutableMap["mimetype"] = it.selectedIndividualHashMap!!["mimetype"]!!
}
imageLoader.loadImage(holder.itemView.previewImage, previewMessageUrl, mutableMap)
}
} ?: run {
holder.itemView.quotedPreviewImage.visibility = View.GONE
}
imageLoader.loadImage(holder.itemView.quotedUserAvatar, parentMessage.user.avatar)
holder.itemView.quotedAuthor.text = parentMessage.actorDisplayName
?: context.getText(R.string.nc_nick_guest)
holder.itemView.quotedChatText.text = parentMessage.text
holder.itemView.messageTime?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
} ?: run {
holder.itemView.quotedMessageLayout.isVisible = false
}
it.imageUrl?.let { imageUrl ->
holder.itemView.previewImage.setOnClickListener {
onElementClickPass?.invoke(page, holder, element, emptyMap())
true
}
if (imageUrl == "no-preview") {
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
holder.itemView.previewImage.visibility = View.VISIBLE
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(it.selectedIndividualHashMap!!["mimetype"])) {
target(holder.itemView.previewImage)
}
} else {
holder.itemView.previewImage.visibility = View.GONE
}
} else {
holder.itemView.previewImage.visibility = View.VISIBLE
val mutableMap = mutableMapOf<String, String>()
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
mutableMap["mimetype"] = it.selectedIndividualHashMap!!["mimetype"]!!
}
imageLoader.loadImage(holder.itemView.previewImage, imageUrl, mutableMap)
}
} ?: run {
holder.itemView.previewImage.visibility = View.GONE
}
} else {
holder.itemView.systemMessageText.text = it.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()
}
}
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

@ -37,6 +37,7 @@ import android.view.*
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
@ -71,8 +72,6 @@ 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.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.controller_chat.view.*
import kotlinx.android.synthetic.main.lobby_view.view.*
@ -93,7 +92,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
var conversationVoiceCallMenuItem: MenuItem? = null
var conversationVideoMenuItem: MenuItem? = null
private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage>
private lateinit var mentionAutocomplete: Autocomplete<*>
private var shouldShowLobby: Boolean = false
@ -113,14 +111,30 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
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(ChatViewLiveDataSource(viewModel.messagesLiveData))
.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)
messagesAdapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
val layoutManager = view.messagesRecyclerView.layoutManager as LinearLayoutManager
if (layoutManager.findLastVisibleItemPosition() == positionStart - 1) {
view.messagesRecyclerView.post {
view.messagesRecyclerView.smoothScrollToPosition(positionStart + 1)
}
} else {
// show popup
}
}
})
val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
layoutManager.stackFromEnd = true
view.messagesRecyclerView.initRecyclerView(layoutManager, messagesAdapter, true)
viewModel.apply {
conversation.observe(this@ChatView) { conversation ->
setTitle()
@ -151,7 +165,7 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
view.lobbyTextView?.setText(R.string.nc_lobby_waiting)
}
} else {
view.messagesRecyclerView?.visibility = View.GONE
view.messagesRecyclerView?.visibility = View.VISIBLE
view.lobbyView?.visibility = View.GONE
if (isReadOnlyConversation) {
@ -165,8 +179,8 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
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) {
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>, payload: Map<String, String>) {
if (element.type == ChatElementTypes.CHAT_MESSAGE.ordinal) {
element.data?.let { chatElement ->
val chatMessage = chatElement.data as ChatMessage
val currentUser = viewModel.user
@ -228,21 +242,27 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
}
}
private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>) {
private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>, payload: Map<String, String>) {
}
override fun onAttach(view: View) {
super.onAttach(view)
viewModel.view = this
setupViews()
}
override fun onDetach(view: View) {
super.onDetach(view)
viewModel.view = null
}
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater
) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_conversation_plus_filter, menu)
inflater.inflate(R.menu.menu_conversation, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
@ -258,14 +278,14 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
conversationVoiceCallMenuItem?.isVisible = true
conversationVideoMenuItem?.isVisible = true
}
if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == viewModel.conversation.value?.type) {
loadAvatar()
}
}
private fun setupViews() {
view?.let { view ->
view.messagesRecyclerView.initRecyclerView(
LinearLayoutManager(view.context), recyclerViewAdapter, false
)
view.popupBubbleView.setRecyclerView(view.messagesRecyclerView)
val filters = arrayOfNulls<InputFilter>(1)
@ -416,30 +436,32 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
private fun loadAvatar() {
val imageLoader = networkComponents.getImageLoader(viewModel.user)
val avatarSize = DisplayUtils.convertDpToPixel(
conversationVoiceCallMenuItem?.icon!!
.intrinsicWidth.toFloat(), activity!!
)
.toInt()
conversationVoiceCallMenuItem?.let {
val avatarSize = DisplayUtils.convertDpToPixel(
it.icon!!.intrinsicWidth.toFloat(), activity!!
)
.toInt()
avatarSize.let {
val target = object : Target {
override fun onSuccess(result: Drawable) {
super.onSuccess(result)
actionBar?.setIcon(result)
avatarSize.let {
val target = object : Target {
override fun onSuccess(result: Drawable) {
super.onSuccess(result)
actionBar?.setIcon(result)
}
}
viewModel.conversation.value?.let {
val avatarRequest = Images().getRequestForUrl(
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
viewModel.user.baseUrl,
it.name, avatarSize / 2
), viewModel.user, target, this,
CircleCropTransformation()
)
imageLoader.load(avatarRequest)
}
}
viewModel.conversation.value?.let {
val avatarRequest = Images().getRequestForUrl(
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
viewModel.user.baseUrl,
it.name, avatarSize / 2
), viewModel.user, target, this,
CircleCropTransformation()
)
imageLoader.load(avatarRequest)
}
}
}

View File

@ -0,0 +1,41 @@
package com.nextcloud.talk.newarch.features.chat
import androidx.lifecycle.LiveData
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Source
import com.otaliastudios.elements.extensions.MainSource
class ChatViewLiveDataSource<T : ChatElement>(private val data: LiveData<List<T>>, loadingIndicatorsEnabled: Boolean = true, errorIndicatorEnabled: Boolean = false, emptyIndicatorEnabled: Boolean = false) : MainSource<T>(loadingIndicatorsEnabled, errorIndicatorEnabled, emptyIndicatorEnabled) {
override fun onPageOpened(page: Page, dependencies: List<Element<*>>) {
super.onPageOpened(page, dependencies)
if (page.previous() == null) {
postResult(page, data)
}
}
override fun dependsOn(source: Source<*>): Boolean {
return false
}
override fun areContentsTheSame(first: T, second: T): Boolean {
return first == second
}
override fun getElementType(data: T): Int {
return data.elementType.ordinal
}
override fun areItemsTheSame(first: T, second: T): Boolean {
if (first.elementType != second.elementType) {
return false
}
if (first.data is ChatMessage && second.data is ChatMessage) {
return first.data.jsonMessageId == second.data.jsonMessageId
}
return false
}
}

View File

@ -23,36 +23,64 @@
package com.nextcloud.talk.newarch.features.chat
import android.app.Application
import android.text.TextUtils
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.bluelinelabs.conductor.Controller
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ChatOverall
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
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.domain.usecases.GetChatMessagesUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.local.models.User
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.toUser
import com.nextcloud.talk.newarch.local.models.toUserEntity
import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.services.GlobalServiceInterface
import com.nextcloud.talk.newarch.utils.NetworkComponents
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
import retrofit2.Response
class ChatViewModel constructor(application: Application,
private val joinConversationUseCase: JoinConversationUseCase,
private val exitConversationUseCase: ExitConversationUseCase,
private val networkComponents: NetworkComponents,
private val apiErrorHandler: ApiErrorHandler,
private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository,
private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface {
lateinit var user: User
val conversation: MutableLiveData<Conversation?> = MutableLiveData()
var initConversation: Conversation? = null
val messagesLiveData = Transformations.switchMap(conversation) {
it?.let {
messagesRepository.getMessagesWithUserForConversation(it.conversationId!!)
var pastStartingPoint: Long = -1
val futureStartingPoint: MutableLiveData<Long> = MutableLiveData()
private var initConversation: Conversation? = null
val messagesLiveData = Transformations.switchMap(futureStartingPoint) {futureStartingPoint ->
conversation.value?.let {
messagesRepository.getMessagesWithUserForConversationSince(it.databaseId!!, futureStartingPoint).map { chatMessagesList ->
chatMessagesList.map { chatMessage ->
chatMessage.activeUser = user.toUserEntity()
chatMessage.parentMessage?.activeUser = chatMessage.activeUser
if (chatMessage.systemMessageType != null && chatMessage.systemMessageType != ChatMessage.SystemMessageType.DUMMY) {
ChatElement(chatMessage, ChatElementTypes.SYSTEM_MESSAGE)
} else {
ChatElement(chatMessage, ChatElementTypes.CHAT_MESSAGE)
}
}
}
}
}
var conversationPassword: String? = null
var view: Controller? = null
fun init(user: User, conversationToken: String, conversationPassword: String?) {
@ -71,7 +99,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.postValue(conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!))
conversation.token?.let { conversationToken ->
globalService.joinConversation(conversationToken, conversationPassword, this)
}
@ -80,7 +108,79 @@ class ChatViewModel constructor(application: Application,
}
override suspend fun joinedConversationForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) {
if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) {
pullPastMessagesForUserAndConversation(userNgEntity, conversation)
}
}
}
private suspend fun pullPastMessagesForUserAndConversation(userNgEntity: UserNgEntity, conversation: Conversation) {
if (userNgEntity.id == user.id && conversation.token == initConversation?.token && view != null) {
val getChatMessagesUseCase = GetChatMessagesUseCase(networkComponents.getRepository(true, userNgEntity.toUser()), apiErrorHandler)
val lastReadMessageId = conversation.lastReadMessageId
getChatMessagesUseCase.invoke(viewModelScope, parametersOf(user, conversation.token, 0, lastReadMessageId, 1), object : UseCaseResponse<Response<ChatOverall>> {
override suspend fun onSuccess(result: Response<ChatOverall>) {
val messages = result.body()?.ocs?.data
messages?.let {
for (message in it) {
message.activeUser = userNgEntity
message.internalConversationId = conversation.databaseId
}
messagesRepository.saveMessagesForConversation(it)
}
val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given")
if (xChatLastGivenHeader != null) {
pastStartingPoint = xChatLastGivenHeader.toLong()
}
futureStartingPoint.postValue(pastStartingPoint)
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, pastStartingPoint.toInt())
}
override suspend fun onError(errorModel: ErrorModel?) {
// What to do here
}
})
}
}
suspend fun pullFutureMessagesForUserAndConversation(userNgEntity: UserNgEntity, conversation: Conversation, lastGivenMessage: Int = 0) {
if (userNgEntity.id == user.id && conversation.token == initConversation?.token && view != null) {
val getChatMessagesUseCase = GetChatMessagesUseCase(networkComponents.getRepository(true, userNgEntity.toUser()), apiErrorHandler)
var lastKnownMessageId = lastGivenMessage
if (lastGivenMessage == 0) {
lastKnownMessageId = conversation.lastReadMessageId.toInt()
}
getChatMessagesUseCase.invoke(viewModelScope, parametersOf(user, conversation.token, 1, lastKnownMessageId, 0), object : UseCaseResponse<Response<ChatOverall>> {
override suspend fun onSuccess(result: Response<ChatOverall>) {
val messages = result.body()?.ocs?.data
messages?.let {
for (message in it) {
message.activeUser = userNgEntity
message.internalConversationId = conversation.databaseId
}
messagesRepository.saveMessagesForConversation(it)
}
if (result.code() == 200) {
val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given")
if (xChatLastGivenHeader != null) {
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, xChatLastGivenHeader.toInt())
}
} else {
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, lastKnownMessageId)
}
}
override suspend fun onError(errorModel: ErrorModel?) {
pullFutureMessagesForUserAndConversation(userNgEntity, conversation)
}
})
}
}
}

View File

@ -25,16 +25,18 @@ package com.nextcloud.talk.newarch.features.chat
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
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.services.GlobalService
import com.nextcloud.talk.newarch.utils.NetworkComponents
class ChatViewModelFactory constructor(
private val application: Application,
private val joinConversationUseCase: JoinConversationUseCase,
private val exitConversationUseCase: ExitConversationUseCase,
private val networkComponents: NetworkComponents,
private val apiErrorHandler: ApiErrorHandler,
private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository,
private val globalService: GlobalService
@ -42,7 +44,7 @@ class ChatViewModelFactory constructor(
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ChatViewModel(
application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService
application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService
) as T
}
}

View File

@ -1,12 +0,0 @@
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

@ -45,6 +45,7 @@ import com.nextcloud.talk.newarch.features.contactsflow.ContactsViewOperationSta
import com.nextcloud.talk.newarch.features.contactsflow.ParticipantElement
import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView
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.newarch.utils.ElementPayload
@ -179,10 +180,13 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
ContactsViewOperationState.OK -> {
val bundle = Bundle()
if (!hasToken || isNewGroupConversation) {
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
router.replaceTopController(RouterTransaction.with(ChatView(bundle))
.popChangeHandler(HorizontalChangeHandler())
.pushChangeHandler(HorizontalChangeHandler()))
globalService.currentUserLiveData.value?.let {
bundle.putParcelable(BundleKeys.KEY_USER, it.toUser())
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
router.replaceTopController(RouterTransaction.with(ChatView(bundle))
.popChangeHandler(HorizontalChangeHandler())
.pushChangeHandler(HorizontalChangeHandler()))
}
} else {
// we added the participants - go back to conversations info
router.popCurrentController()

View File

@ -155,6 +155,7 @@ open class ConversationPresenter(context: Context, onElementClick: ((Page, Holde
addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation())
fallback(Images().getImageForConversation(context, conversation, true))
error(Images().getImageForConversation(context, conversation, true))
}
}
}

View File

@ -22,6 +22,7 @@
package com.nextcloud.talk.newarch.features.conversationsList
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
@ -82,7 +83,7 @@ class ConversationsListView : BaseView() {
val adapter = Adapter.builder(this)
.addSource(ConversationsListSource(viewModel.conversationsLiveData))
.addPresenter(ConversationPresenter(context, ::onElementClick, ::onElementLongClick))
.addPresenter(ConversationPresenter(activity as Context, ::onElementClick, ::onElementLongClick))
.addPresenter(Presenter.forLoadingIndicator(context, R.layout.loading_state))
.addPresenter(AdvancedEmptyPresenter(context, R.layout.message_state, ::openNewConversationScreen) { view ->
view.messageStateImageView.imageTintList = resources?.getColor(R.color.colorPrimary)?.let { ColorStateList.valueOf(it) }
@ -163,7 +164,7 @@ class ConversationsListView : BaseView() {
conversation?.let { conversation ->
val bundle = Bundle()
with(bundle) {
putParcelable(BundleKeys.KEY_USER_ENTITY, user)
putParcelable(BundleKeys.KEY_USER, user.toUser())
putString(BundleKeys.KEY_CONVERSATION_TOKEN, conversation.token)
putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId)
putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))

View File

@ -179,11 +179,13 @@ class ConversationsListViewModel (
operationUser?.let {
viewModelScope.launch {
val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 256)
val drawable = Coil.get((url)) {
addHeader("Authorization", it.getCredentials())
transformations(CircleCropTransformation())
}
avatar.postValue(drawable)
try {
val drawable = Coil.get((url)) {
addHeader("Authorization", it.getCredentials())
transformations(CircleCropTransformation())
}
avatar.postValue(drawable)
} catch (e: Exception) {}
}
}
}

View File

@ -0,0 +1,24 @@
package com.nextcloud.talk.newarch.local.converters
import androidx.room.TypeConverter
import com.bluelinelabs.logansquare.LoganSquare
class HashMapHashMapConverter {
@TypeConverter
fun fromDoubleHashMapToString(map: HashMap<String, HashMap<String, String>>?): String? {
if (map == null) {
return ""
}
return LoganSquare.serialize(map)
}
@TypeConverter
fun fromStringToDoubleHashMap(value: String?): HashMap<String, HashMap<String, String>>? {
if (value.isNullOrEmpty()) {
return null
}
return LoganSquare.parseMap(value, HashMap::class.java) as HashMap<String, HashMap<String, String>>?
}
}

View File

@ -80,8 +80,8 @@ abstract class ConversationsDao {
timestamp: Long
)
@Query("SELECT * FROM conversations where id = :internalUserId AND token = :token")
abstract suspend fun getConversationForUserWithToken(internalUserId: Long, token: String): ConversationEntity?
@Query("SELECT * FROM conversations where user_id = :userId AND token = :token")
abstract suspend fun getConversationForUserWithToken(userId: Long, token: String): ConversationEntity?
@Transaction
open suspend fun updateConversationsForUser(

View File

@ -31,10 +31,13 @@ import com.nextcloud.talk.newarch.local.models.MessageEntity
@Dao
abstract class MessagesDao {
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId")
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId ORDER BY message_id ASC")
abstract fun getMessagesWithUserForConversation(conversationId: String):
LiveData<List<MessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveMessagesWithInsert(vararg messages: MessageEntity): List<Long>
abstract suspend fun saveMessages(vararg messages: MessageEntity): List<Long>
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND message_id >= :messageId ORDER BY message_id ASC")
abstract fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<MessageEntity>>
}

View File

@ -34,6 +34,7 @@ import com.nextcloud.talk.newarch.local.dao.UsersDao
import com.nextcloud.talk.newarch.local.models.ConversationEntity
import com.nextcloud.talk.newarch.local.models.MessageEntity
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import org.parceler.converter.HashMapParcelConverter
@Database(
entities = [ConversationEntity::class, MessageEntity::class, UserNgEntity::class],
@ -46,7 +47,8 @@ import com.nextcloud.talk.newarch.local.models.UserNgEntity
ConversationTypeConverter::class, ParticipantTypeConverter::class,
PushConfigurationConverter::class, CapabilitiesConverter::class,
SignalingSettingsConverter::class,
UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class
UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class,
HashMapHashMapConverter::class
)
abstract class TalkDatabase : RoomDatabase() {

View File

@ -48,13 +48,12 @@ data class MessageEntity(
@ColumnInfo(name = "actor_display_name") var actorDisplayName: String? = null,
@ColumnInfo(name = "timestamp") var timestamp: Long = 0,
@ColumnInfo(name = "message") var message: String? = null,
/*@JsonField(name = "messageParameters")
public HashMap<String, HashMap<String, String>> messageParameters;*/
@ColumnInfo(name = "messageParameters") var messageParameters: HashMap<String, HashMap<String, String>>? = null,
@ColumnInfo(name = "parent") var parentMessage: ChatMessage? = null,
@ColumnInfo(name = "replyable") var replyable: Boolean = false,
@ColumnInfo(name = "system_message_type") var systemMessageType: SystemMessageType? = null
)
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
fun MessageEntity.toChatMessage(): ChatMessage {
val chatMessage = ChatMessage()
chatMessage.internalMessageId = this.id
@ -65,15 +64,15 @@ fun MessageEntity.toChatMessage(): ChatMessage {
chatMessage.actorDisplayName = this.actorDisplayName
chatMessage.timestamp = this.timestamp
chatMessage.message = this.message
//chatMessage.messageParameters = this.messageParameters
chatMessage.messageParameters = this.messageParameters
chatMessage.systemMessageType = this.systemMessageType
chatMessage.replyable = this.replyable
chatMessage.parentMessage = this.parentMessage
return chatMessage
}
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
fun ChatMessage.toMessageEntity(): MessageEntity {
val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.activeUser!!.id.toString() + "@" + this.internalConversationId)
val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.internalConversationId!!)
messageEntity.messageId = this.jsonMessageId!!
messageEntity.actorType = this.actorType
messageEntity.actorId = this.actorId
@ -82,7 +81,8 @@ fun ChatMessage.toMessageEntity(): MessageEntity {
messageEntity.message = this.message
messageEntity.systemMessageType = this.systemMessageType
messageEntity.replyable = this.replyable
//messageEntity.messageParameters = this.messageParameters
messageEntity.messageParameters = this.messageParameters
messageEntity.parentMessage = this.parentMessage
return messageEntity
}

View File

@ -55,7 +55,6 @@ class GlobalService constructor(usersRepository: UsersRepository,
user?.let {
if (it.id != previousUser?.id) {
cookieManager.cookieStore.removeAll()
//okHttpClient.dispatcher().cancelAll()
currentConversation = null
}
}

View File

@ -129,10 +129,16 @@ class ShortcutService constructor(private var context: Context,
iconImage = images.getImageForConversation(context, conversation)
if (iconImage == null) {
iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) {
addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation())
try {
iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) {
addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation())
}
} catch (e: Exception) {
// no icon, that's fine for now
iconImage = images.getImageForConversation(context, conversation, true)
}
}
shortcuts.add(ShortcutInfoCompat.Builder(context, "current_conversation_" + (index + 1))

View File

@ -47,6 +47,7 @@ class NetworkComponents(
val usersMultipleOperationsRepositoryMap: MutableMap<Long, NextcloudTalkRepository> = mutableMapOf()
val usersSingleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
val usersMultipleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
val usersImageLoaderMap: MutableMap<Long, ImageLoader> = mutableMapOf()
fun getRepository(singleOperation: Boolean, user: User): NextcloudTalkRepository {
val mappedNextcloudTalkRepository = if (singleOperation) {
@ -89,20 +90,28 @@ class NetworkComponents(
}
fun getImageLoader(user: User): ImageLoader {
return ImageLoader(androidApplication) {
availableMemoryPercentage(0.5)
bitmapPoolPercentage(0.5)
crossfade(false)
okHttpClient(getOkHttpClient(false, user))
componentRegistry {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder())
} else {
add(GifDecoder())
var mappedImageLoader = usersImageLoaderMap[user.id]
if (mappedImageLoader == null) {
mappedImageLoader = ImageLoader(androidApplication) {
availableMemoryPercentage(0.5)
bitmapPoolPercentage(0.5)
crossfade(false)
okHttpClient(getOkHttpClient(false, user))
componentRegistry {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder())
} else {
add(GifDecoder())
}
add(SvgDecoder(androidApplication))
}
add(SvgDecoder(androidApplication))
}
usersImageLoaderMap[user.id!!] = mappedImageLoader
}
return mappedImageLoader
}
}

View File

@ -51,15 +51,6 @@ public class ArbitraryStorageUtils {
.subscribe();
}
public ArbitraryStorageEntity getStorageSetting(long accountIdentifier, String key,
@Nullable String object) {
Result findStorageQueryResult = dataStore.select(ArbitraryStorage.class)
.where(ArbitraryStorageEntity.ACCOUNT_IDENTIFIER.eq(accountIdentifier)
.and(ArbitraryStorageEntity.KEY.eq(key)).and(ArbitraryStorageEntity.OBJECT.eq(object)))
.limit(1).get();
return (ArbitraryStorageEntity) findStorageQueryResult.firstOrNull();
}
public Observable deleteAllEntriesForAccountIdentifier(long accountIdentifier) {
ReactiveScalar<Integer> deleteResult = dataStore.delete(ArbitraryStorage.class)

View File

@ -1,45 +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.utils.preferences.preferencestorage;
import android.content.Context;
import com.nextcloud.talk.interfaces.ConversationInfoInterface;
import com.nextcloud.talk.newarch.local.models.UserNgEntity;
import com.yarolegovich.mp.io.StorageModule;
public class DatabaseStorageFactory implements StorageModule.Factory {
private UserNgEntity conversationUser;
private String conversationToken;
private ConversationInfoInterface conversationInfoInterface;
public DatabaseStorageFactory(UserNgEntity conversationUser, String conversationToken,
ConversationInfoInterface conversationInfoInterface) {
this.conversationUser = conversationUser;
this.conversationToken = conversationToken;
this.conversationInfoInterface = conversationInfoInterface;
}
@Override
public StorageModule create(Context context) {
return new DatabaseStorageModule(conversationUser, conversationToken, conversationInfoInterface);
}
}

View File

@ -1,268 +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.utils.preferences.preferencestorage
import android.os.Bundle
import android.text.TextUtils
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.interfaces.ConversationInfoInterface
import com.nextcloud.talk.models.database.ArbitraryStorageEntity
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageUtils
import com.yarolegovich.mp.io.StorageModule
import io.reactivex.Observer
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.inject
class DatabaseStorageModule(
private val conversationUser: UserNgEntity,
private val conversationToken: String,
private val conversationInfoInterface: ConversationInfoInterface
) : StorageModule, KoinComponent {
val arbitraryStorageUtils: ArbitraryStorageUtils by inject()
val ncApi: NcApi by inject()
private val accountIdentifier: Long
private var lobbyValue = false
private var favoriteConversationValue = false
private var allowGuestsValue = false
private var hasPassword: Boolean? = null
private var conversationNameValue: String? = null
private var messageNotificationLevel: String? = null
override fun saveBoolean(
key: String,
value: Boolean
) {
if (key != "conversation_lobby" && key != "allow_guests" && key != "favorite_conversation"
) {
arbitraryStorageUtils.storeStorageSetting(
accountIdentifier, key, value.toString(),
conversationToken
)
} else {
when (key) {
"conversation_lobby" -> lobbyValue = value
"allow_guests" -> allowGuestsValue = value
"favorite_conversation" -> favoriteConversationValue = value
else -> {
}
}
}
}
override fun saveString(
key: String,
value: String
) {
if (key != "message_notification_level"
&& key != "conversation_name"
&& key != "conversation_password"
) {
arbitraryStorageUtils.storeStorageSetting(accountIdentifier, key, value, conversationToken)
} else {
if (key == "message_notification_level") {
if (conversationUser.hasSpreedFeatureCapability("notification-levels")) {
if (!TextUtils.isEmpty(
messageNotificationLevel
) && messageNotificationLevel != value
) {
val intValue: Int
intValue = when (value) {
"never" -> 3
"mention" -> 2
"always" -> 1
else -> 0
}
ncApi.setNotificationLevel(
ApiUtils.getCredentials(
conversationUser.username,
conversationUser.token
),
ApiUtils.getUrlForSettingNotificationlevel(
conversationUser.baseUrl,
conversationToken
),
intValue
)
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(genericOverall: GenericOverall) {
messageNotificationLevel = value
}
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
} else {
messageNotificationLevel = value
}
}
} else if (key == "conversation_password") {
if (hasPassword != null) {
ncApi.setPassword(
ApiUtils.getCredentials(
conversationUser.username,
conversationUser.token
),
ApiUtils.getUrlForPassword(
conversationUser.baseUrl,
conversationToken
), value
)
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(genericOverall: GenericOverall) {
hasPassword = !TextUtils.isEmpty(value)
conversationInfoInterface.passwordSet(TextUtils.isEmpty(value))
}
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
} else {
hasPassword = value.toBoolean()
}
} else if (key == "conversation_name") {
if (!TextUtils.isEmpty(
conversationNameValue
) && conversationNameValue != value
) {
ncApi.renameRoom(
ApiUtils.getCredentials(
conversationUser.username,
conversationUser.token
), ApiUtils.getRoom(
conversationUser.baseUrl,
conversationToken
), value
)
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(genericOverall: GenericOverall) {
conversationNameValue = value
conversationInfoInterface.conversationNameSet(value)
}
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
} else {
conversationNameValue = value
}
}
}
}
override fun saveInt(
key: String,
value: Int
) {
arbitraryStorageUtils.storeStorageSetting(
accountIdentifier, key, Integer.toString(value),
conversationToken
)
}
override fun saveStringSet(
key: String,
value: Set<String>
) {
}
override fun getBoolean(
key: String,
defaultVal: Boolean
): Boolean {
return if (key == "conversation_lobby") {
lobbyValue
} else if (key == "allow_guests") {
allowGuestsValue
} else if (key == "favorite_conversation") {
favoriteConversationValue
} else {
val valueFromDb: ArbitraryStorageEntity? =
arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken)
if (valueFromDb == null) {
defaultVal
} else {
valueFromDb.value!!.toBoolean()
}
}
}
override fun getString(
key: String,
defaultVal: String?
): String? {
if (key != "message_notification_level"
&& key != "conversation_name"
&& key != "conversation_password"
) {
val valueFromDb: ArbitraryStorageEntity? =
arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken)
return if (valueFromDb == null) {
defaultVal
} else {
valueFromDb.value
}
} else if (key == "message_notification_level") {
return messageNotificationLevel
} else if (key == "conversation_name") {
return conversationNameValue
} else if (key == "conversation_password") {
return ""
}
return ""
}
override fun getInt(
key: String,
defaultVal: Int
): Int {
val valueFromDb: ArbitraryStorageEntity? =
arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken)
return if (valueFromDb == null) {
defaultVal
} else {
Integer.parseInt(valueFromDb.value)
}
}
override fun getStringSet(
key: String,
defaultVal: Set<String>
): Set<String>? {
return null
}
override fun onSaveInstanceState(outState: Bundle) {}
override fun onRestoreInstanceState(savedState: Bundle) {}
init {
accountIdentifier = conversationUser.id
}
}

View File

@ -32,8 +32,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/separator"
app:stackFromEnd="true"
app:reverseLayout="true"
android:id="@+id/messagesRecyclerView"/>
<View

View File

@ -1,77 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quotedChatMessageView"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/quotedMessageLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp">
android:layout_height="wrap_content">
<View
android:id="@+id/quoteColoredView"
android:layout_width="2dp"
android:layout_height="match_parent"
android:layout_alignBottom="@id/flexboxQuoted"
android:layout_height="100dp"
android:layout_alignParentStart="true"
android:layout_alignBottom="@id/quotedChatText"
android:layout_marginEnd="4dp"
android:background="@color/colorPrimary"/>
android:background="@color/colorPrimary"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/quoteColoredView"
android:layout_marginEnd="8dp"
android:id="@+id/quotedAuthorLayout">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/quotedUserAvatar"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
app:shapeAppearanceOverlay="@style/circleImageView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/quotedUserAvatar"
android:textSize="12sp"
android:id="@+id/quotedAuthor"
android:layout_alignBaseline="@id/quotedUserAvatar"
tools:text="Another user"/>
</RelativeLayout>
<ImageView
android:id="@+id/quotedPreviewImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/quotedAuthorLayout"
android:adjustViewBounds="true"
android:layout_alignParentStart="true"
android:scaleType="fitCenter"
android:layout_marginStart="8dp"
android:layout_toStartOf="@id/cancelReplyButton"
android:layout_marginTop="8dp"
tools:src="@tools:sample/backgrounds/scenic"/>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/quotedMessageAuthor"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:textSize="12sp"
tools:text="Mario" />
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flexboxQuoted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/quotedMessageAuthor"
android:layout_alignStart="@id/quotedMessageAuthor"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
android:layout_toStartOf="@id/cancelReplyButton"
android:layout_marginTop="4dp"
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_start">
android:id="@+id/quotedChatText"
android:layout_below="@id/quotedPreviewImage"
tools:text="Just another chat message"/>
<ImageView
android:id="@+id/quotedMessageImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/quotedChatText"
android:textSize="12sp"
android:id="@+id/quotedMessageTime"
android:layout_marginEnd="8dp"
tools:text="12:30"/>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/quotedMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/quotedMessageImage"
android:layout_alignStart="@id/quotedMessageAuthor"
android:lineSpacingMultiplier="1.2"
android:textSize="14sp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
tools:text="Hello, this is me!" />
</com.google.android.flexbox.FlexboxLayout>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginHorizontal="8dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:background="@drawable/ic_cancel_black_24dp"

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/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

@ -1,79 +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="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="2dp">
<ImageView
android:id="@id/messageUserAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp" />
<com.google.android.flexbox.FlexboxLayout
android:id="@id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
android:layout_toEndOf="@id/messageUserAvatar"
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/messageAuthor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textColor="@color/colorPrimary"
android:textSize="12sp" />
<include layout="@layout/item_message_quote" android:visibility="gone"/>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/incomingMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="false"
android:autoLink="all"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
<TextView
android:id="@+id/incomingMessageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/outgoingMessageText"
android:layout_marginStart="8dp"
app:layout_alignSelf="center" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -0,0 +1,77 @@
<?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="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:id="@+id/authorLayout">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/authorAvatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_marginEnd="8dp"
android:layout_centerVertical="true"
app:shapeAppearanceOverlay="@style/circleImageView"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toEndOf="@id/authorAvatar"
android:textSize="12sp"
android:id="@+id/authorName"
android:layout_centerVertical="true"
android:gravity="center_vertical"
tools:text="Regular user"/>
</RelativeLayout>
<include layout="@layout/item_message_quote"
android:layout_below="@id/authorLayout"
android:layout_marginTop="4dp"
android:layout_marginStart="40dp"
android:layout_marginEnd="8dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<ImageView
android:id="@+id/previewImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_marginTop="4dp"
android:layout_alignParentStart="true"
android:scaleType="fitCenter"
android:layout_below="@id/quotedMessageLayout"
android:layout_marginStart="40dp"
android:layout_marginEnd="8dp"
tools:src="@tools:sample/backgrounds/scenic"/>
<androidx.emoji.widget.EmojiTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginEnd="8dp"
android:id="@+id/chatMessage"
android:layout_below="@id/previewImage"
tools:text="Just another chat message"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/chatMessage"
android:textSize="12sp"
android:id="@+id/messageTime"
android:layout_marginEnd="8dp"
tools:text="12:30"/>
</RelativeLayout>

View File

@ -1,77 +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="match_parent"
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_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical"
android:layout_alignParentEnd="true"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<ImageView
android:id="@+id/outgoingPreviewImage"
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/outgoingPreviewMessageText"
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/outgoingPreviewTime"
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>
</RelativeLayout>

View File

@ -1,65 +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="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="2dp">
<com.google.android.flexbox.FlexboxLayout
android:id="@id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
<include layout="@layout/item_message_quote" android:visibility="gone"/>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/outgoingMessageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true"
android:lineSpacingMultiplier="1.2"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true"
android:textColorHighlight="@color/nc_grey"
android:textIsSelectable="false"
android:autoLink="all"/>
<TextView
android:id="@+id/outgoingMessageTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/outgoingMessageText"
android:layout_marginStart="8dp"
app:layout_alignSelf="center" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -1,12 +1,14 @@
<?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">
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_centerHorizontal="true">
<TextView
android:id="@+id/noticeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textAlignment="center"
android:padding="16dp"/>
</RelativeLayout>

View File

@ -36,7 +36,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0-alpha03'
classpath 'com.android.tools.build:gradle:4.1.0-alpha04'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"

View File

@ -1,6 +1,6 @@
#Thu Mar 12 14:36:26 CET 2020
#Sat Apr 04 13:17:10 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-bin.zip