diff --git a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json index 027fe5f1e..bb2cd2235 100644 --- a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json +++ b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json @@ -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')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 077392cd1..2f9673f15 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -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, diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index a4459dc87..77ced1c75 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -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>? = 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(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(R.id.mp_checkable) as SwitchCompat).isChecked) { ncApi.addConversationToFavorites(conversationUser.getCredentials(), ApiUtils .getUrlForConversationFavorites(conversationUser.baseUrl, conversation!!.token)) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt index e2af942ca..4b9ece57c 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/MessagesRepositoryImpl.kt @@ -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> { - 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> { + return messagesDao.getMessagesWithUserForConversationSince(conversationId, messageId).distinctUntilChanged().map { + it.map { messageEntity -> + messageEntity.toChatMessage() + } + } + } + + override suspend fun saveMessagesForConversation(messages: List): List { + val updatedMessages = messages.map { + it.toMessageEntity() + } + + return messagesDao.saveMessages(*updatedMessages.toTypedArray()) } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt index b56fd90bc..809e0f779 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt @@ -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 { + val mutableMap = mutableMapOf() + 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)) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt index 65ac4470a..cf54aea30 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt @@ -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): Response + @GET suspend fun getPeersForCall(@Header("Authorization") authorization: String, @Url url: String): ParticipantsOverall diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt index ce54ff5f1..50e7a2740 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/di/module/UseCasesModule.kt @@ -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) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt index 20bda90ac..1e881dcd3 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/MessagesRepository.kt @@ -27,5 +27,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage interface MessagesRepository { fun getMessagesWithUserForConversation(conversationId: String): LiveData> - + fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData> + suspend fun saveMessagesForConversation(messages: List): List } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt index 53180db27..c4df6c747 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt @@ -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 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 diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetChatMessagesUseCase.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetChatMessagesUseCase.kt new file mode 100644 index 000000000..8d71058dc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetChatMessagesUseCase.kt @@ -0,0 +1,40 @@ +/* + * + * * Nextcloud Talk application + * * + * * @author Mario Danic + * * Copyright (C) 2017-2020 Mario Danic + * * + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + * + */ + +package com.nextcloud.talk.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, Any?>(apiErrorHandler) { + override suspend fun run(params: Any?): Response { + val definitionParameters = params as DefinitionParameters + return nextcloudTalkRepository.getChatMessagesForConversation(definitionParameters[0], definitionParameters[1], definitionParameters[2], definitionParameters[3], definitionParameters[4]) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt index 449d144f1..b25241318 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatDateHeaderSource.kt @@ -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() - override fun dependsOn(source: Source<*>) = source is ChatViewSource + override fun dependsOn(source: Source<*>) = source is ChatViewLiveDataSource override fun getElementType(data: Data): Int { return elementType diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt index a4f85186e..2d5f2ae5d 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElement.kt @@ -2,5 +2,5 @@ package com.nextcloud.talk.newarch.features.chat data class ChatElement( val data: Any, - val elementType: Int + val elementType: ChatElementTypes ) \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt index 7a658879f..8e66fd546 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatElementTypes.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt index 3b2c25b2a..fe07e0a54 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatPresenter.kt @@ -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(context: Context, onElementClick: ((Page, Holder, Element) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter(context, onElementClick), KoinComponent { +open class ChatPresenter(context: Context, private val onElementClickPass: ((Page, Holder, Element, Map) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element, Map) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter(context), KoinComponent { override val elementTypes: Collection - get() = listOf(ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal, ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal, ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.SYSTEM_MESSAGE.ordinal, ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal, ChatElementTypes.DATE_HEADER.ordinal) + 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(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(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(2) - layers[0] = context.getDrawable(R.drawable.ic_launcher_background) - layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground) - val layerDrawable = LayerDrawable(layers) - val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable)) - imageLoader.getImageLoader().load(loadBuilder.build()) - } else if (it.actorType == "bots") { - holder.itemView.messageUserAvatar.isVisible = true - val drawable = TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRound( - ">", - context.resources.getColor(R.color.black) - ) - val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(drawable)) - imageLoader.getImageLoader().load(loadBuilder.build()) - } else if (!it.grouped && !it.oneToOneConversation) { - holder.itemView.messageUserAvatar.isVisible = true - imageLoader.loadImage(holder.itemView.messageUserAvatar, it.user.avatar) - } else { - holder.itemView.messageUserAvatar.isVisible = false - } - } else { - holder.itemView.outgoingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) - holder.itemView.outgoingMessageText.text = it.text - } - - it.parentMessage?.let { parentMessage -> - parentMessage.imageUrl?.let { previewMessageUrl -> - holder.itemView.quotedMessageImage.visibility = View.VISIBLE - imageLoader.loadImage(holder.itemView.quotedMessageImage, previewMessageUrl) - } ?: run { - holder.itemView.quotedMessageImage.visibility = View.GONE - } - - holder.itemView.quotedMessageAuthor.text = parentMessage.actorDisplayName ?: context.getText(R.string.nc_nick_guest) - holder.itemView.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.colorPrimary)) - holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary) - holder.itemView.quotedChatMessageView.visibility = View.VISIBLE - } ?: run { - holder.itemView.quotedChatMessageView.visibility = View.GONE - } - - } else if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) { - var previewAvailable = true - val mutableMap = mutableMapOf() - if (it.selectedIndividualHashMap!!.containsKey("mimetype")) { - mutableMap.put("mimetype", it.selectedIndividualHashMap!!["mimetype"]!!) - if (it.imageUrl == "no-preview") { - previewAvailable = false - imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(chatMessage.selectedIndividualHashMap!!["mimetype"])) - } - } - - // Before someone tells me parts of this can be refactored so there is less code: - // YES, I KNOW! - // But the way it's done now means pretty much anyone can understand it and it's easy - // to modify. Prefer simplicity over complexity wherever possible - - if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal) { - if (previewAvailable) { - imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!) - } - if (!it.grouped && !it.oneToOneConversation) { - holder.itemView.messageUserAvatar.visibility = View.GONE - } else { - holder.itemView.messageUserAvatar.visibility = View.VISIBLE - imageLoader.loadImage(holder.itemView.messageUserAvatar, chatMessage.user.avatar) - } - - when (it.messageType) { - ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { - holder.itemView.incomingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"] - } - ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> { - holder.itemView.incomingPreviewMessageText.text = "GIPHY" - } - ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> { - holder.itemView.incomingPreviewMessageText.text = "TENOR" - } - else -> { - holder.itemView.incomingPreviewMessageText.text = "" - } - } - - holder.itemView.incomingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) - } else { - if (previewAvailable) { - imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!) - } - - when (it.messageType) { - ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { - holder.itemView.outgoingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"] - } - ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> { - holder.itemView.outgoingPreviewMessageText.text = "GIPHY" - } - ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> { - holder.itemView.outgoingPreviewMessageText.text = "TENOR" - } - else -> { - holder.itemView.outgoingPreviewMessageText.text = "" - } - } - - holder.itemView.outgoingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) - } + if (it.actorType == "bots" && it.actorId == "changelog") { + val layers = arrayOfNulls(2) + layers[0] = context.getDrawable(R.drawable.ic_launcher_background) + layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + 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() + 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() + 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() } } } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt index 459c029fd..187431e38 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatView.kt @@ -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 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) { - 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, payload: Map) { + 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) { + private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element, payload: Map) { } 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(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) - } } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt new file mode 100644 index 000000000..11f888de6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewLiveDataSource.kt @@ -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(private val data: LiveData>, loadingIndicatorsEnabled: Boolean = true, errorIndicatorEnabled: Boolean = false, emptyIndicatorEnabled: Boolean = false) : MainSource(loadingIndicatorsEnabled, errorIndicatorEnabled, emptyIndicatorEnabled) { + override fun onPageOpened(page: Page, dependencies: List>) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt index f761cfec6..0727fd068 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModel.kt @@ -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(application), GlobalServiceInterface { lateinit var user: User val conversation: MutableLiveData = MutableLiveData() - var initConversation: Conversation? = null - val messagesLiveData = Transformations.switchMap(conversation) { - it?.let { - messagesRepository.getMessagesWithUserForConversation(it.conversationId!!) + var pastStartingPoint: Long = -1 + val futureStartingPoint: MutableLiveData = 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> { + override suspend fun onSuccess(result: Response) { + 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> { + override suspend fun onSuccess(result: Response) { + 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) + } + }) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModelFactory.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModelFactory.kt index fcf9c9db8..3c1e4296e 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModelFactory.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewModelFactory.kt @@ -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 create(modelClass: Class): T { return ChatViewModel( - application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService + application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService ) as T } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt deleted file mode 100644 index 32ec1b631..000000000 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/chat/ChatViewSource.kt +++ /dev/null @@ -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(loadingIndicatorsEnabled: Boolean = true, errorIndicatorEnabled: Boolean = false, emptyIndicatorEnabled: Boolean = false) : MainSource(loadingIndicatorsEnabled, errorIndicatorEnabled, emptyIndicatorEnabled) { - override fun areItemsTheSame(first: T, second: T): Boolean { - TODO("Not yet implemented") - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt index 8a377355b..6ce37aeab 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/contactsflow/contacts/ContactsView.kt @@ -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() diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationPresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationPresenter.kt index 8d99e80d1..f8c2685fb 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationPresenter.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationPresenter.kt @@ -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)) } } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt index 25fc32ab9..59883185d 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt @@ -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)) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt index afb020c9b..4c67254ab 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt @@ -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) {} } } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/converters/HashMapHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/converters/HashMapHashMapConverter.kt new file mode 100644 index 000000000..5a48ae328 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/converters/HashMapHashMapConverter.kt @@ -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? { + if (map == null) { + return "" + } + + return LoganSquare.serialize(map) + } + + @TypeConverter + fun fromStringToDoubleHashMap(value: String?): HashMap>? { + if (value.isNullOrEmpty()) { + return null + } + + return LoganSquare.parseMap(value, HashMap::class.java) as HashMap>? + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt index 586004fca..09413444f 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/ConversationsDao.kt @@ -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( diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt index f48296145..cf85cf1fa 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/MessagesDao.kt @@ -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> @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract suspend fun saveMessagesWithInsert(vararg messages: MessageEntity): List + abstract suspend fun saveMessages(vararg messages: MessageEntity): List + + @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> } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt index cdc0698da..476655201 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/db/TalkDatabase.kt @@ -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() { diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt index 68368851b..c43fceca5 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt @@ -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> messageParameters;*/ + @ColumnInfo(name = "messageParameters") var messageParameters: HashMap>? = 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 } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt b/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt index d5b055b88..c3e3d112e 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/services/GlobalService.kt @@ -55,7 +55,6 @@ class GlobalService constructor(usersRepository: UsersRepository, user?.let { if (it.id != previousUser?.id) { cookieManager.cookieStore.removeAll() - //okHttpClient.dispatcher().cancelAll() currentConversation = null } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/services/shortcuts/ShortcutService.kt b/app/src/main/java/com/nextcloud/talk/newarch/services/shortcuts/ShortcutService.kt index b88f6b0d6..378453ba6 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/services/shortcuts/ShortcutService.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/services/shortcuts/ShortcutService.kt @@ -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)) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkComponents.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkComponents.kt index 822f1a301..f40d69142 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkComponents.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkComponents.kt @@ -47,6 +47,7 @@ class NetworkComponents( val usersMultipleOperationsRepositoryMap: MutableMap = mutableMapOf() val usersSingleOperationOkHttpMap: MutableMap = mutableMapOf() val usersMultipleOperationOkHttpMap: MutableMap = mutableMapOf() + val usersImageLoaderMap: MutableMap = 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java index 2f6ebc40b..32f672a79 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java @@ -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 deleteResult = dataStore.delete(ArbitraryStorage.class) diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java deleted file mode 100644 index 134df989e..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt deleted file mode 100644 index 0e97fb2f9..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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 { - 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 { - 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 { - 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 - ) { - } - - 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 - ): Set? { - return null - } - - override fun onSaveInstanceState(outState: Bundle) {} - override fun onRestoreInstanceState(savedState: Bundle) {} - - init { - accountIdentifier = conversationUser.id - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/controller_chat.xml b/app/src/main/res/layout/controller_chat.xml index 5e53d090e..4d3b444fd 100644 --- a/app/src/main/res/layout/controller_chat.xml +++ b/app/src/main/res/layout/controller_chat.xml @@ -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"/> + android:layout_height="wrap_content"> + android:background="@color/colorPrimary" + /> + + + + + + + + + + - - + android:id="@+id/quotedChatText" + android:layout_below="@id/quotedPreviewImage" + tools:text="Just another chat message"/> - + - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/rv_chat_incoming_text_item.xml b/app/src/main/res/layout/rv_chat_incoming_text_item.xml deleted file mode 100644 index 18a26cd08..000000000 --- a/app/src/main/res/layout/rv_chat_incoming_text_item.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/rv_chat_item.xml b/app/src/main/res/layout/rv_chat_item.xml new file mode 100644 index 000000000..c7529dde9 --- /dev/null +++ b/app/src/main/res/layout/rv_chat_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/rv_chat_outgoing_preview_item.xml b/app/src/main/res/layout/rv_chat_outgoing_preview_item.xml deleted file mode 100644 index 8a38e3330..000000000 --- a/app/src/main/res/layout/rv_chat_outgoing_preview_item.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/rv_chat_outgoing_text_item.xml b/app/src/main/res/layout/rv_chat_outgoing_text_item.xml deleted file mode 100644 index 3a5913307..000000000 --- a/app/src/main/res/layout/rv_chat_outgoing_text_item.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/rv_date_and_unread_notice_item.xml b/app/src/main/res/layout/rv_date_and_unread_notice_item.xml index 2d68aa94f..ff1ef434a 100644 --- a/app/src/main/res/layout/rv_date_and_unread_notice_item.xml +++ b/app/src/main/res/layout/rv_date_and_unread_notice_item.xml @@ -1,12 +1,14 @@ + android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_centerHorizontal="true"> \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5848f8da3..18df8cec3 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 76ecfb040..897730557 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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