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, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "4976b952409bfae25e1f9bc8df18c11c", "identityHash": "4e8c1ae6a440d8491937afe33a3ab085",
"entities": [ "entities": [
{ {
"tableName": "conversations", "tableName": "conversations",
@ -212,7 +212,7 @@
}, },
{ {
"tableName": "messages", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -262,6 +262,18 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentMessage",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{ {
"fieldPath": "replyable", "fieldPath": "replyable",
"columnName": "replyable", "columnName": "replyable",
@ -389,7 +401,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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.contactsflow.contacts.ContactsView
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListView import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListView
import com.nextcloud.talk.newarch.local.models.UserNgEntity 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.ConductorRemapping
import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.bundle.BundleKeys 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 // due to complications with persistablebundle not supporting complex types we do this magic
// remove this once we rewrite chat magic // remove this once we rewrite chat magic
val extras = intent.extras!! val extras = intent.extras!!
extras.putParcelable(BundleKeys.KEY_USER_ENTITY, it) extras.putParcelable(BundleKeys.KEY_USER, it.toUser())
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
ConductorRemapping.remapChatController( ConductorRemapping.remapChatController(
router!!, it.id, 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.DisplayUtils
import com.nextcloud.talk.utils.ShareUtils import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
import com.nextcloud.talk.utils.ui.MaterialPreferenceCategoryWithRightLink import com.nextcloud.talk.utils.ui.MaterialPreferenceCategoryWithRightLink
import com.yarolegovich.lovelydialog.LovelySaveStateHandler import com.yarolegovich.lovelydialog.LovelySaveStateHandler
import com.yarolegovich.lovelydialog.LovelyStandardDialog import com.yarolegovich.lovelydialog.LovelyStandardDialog
@ -190,7 +189,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
private var roomDisposable: Disposable? = null private var roomDisposable: Disposable? = null
private var participantsDisposable: Disposable? = null private var participantsDisposable: Disposable? = null
private var databaseStorageModule: DatabaseStorageModule? = null
private var conversation: Conversation? = null private var conversation: Conversation? = null
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
@ -253,15 +251,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
saveStateHandler = LovelySaveStateHandler() saveStateHandler = LovelySaveStateHandler()
} }
if (databaseStorageModule == null) {
databaseStorageModule = DatabaseStorageModule(
conversationUser!!, conversationToken!!, this)
}
notificationsPreferenceScreen.setStorageModule(databaseStorageModule)
conversationInfoWebinar.setStorageModule(databaseStorageModule)
generalConversationOptions.setStorageModule(databaseStorageModule)
actionTextView.visibility = View.GONE actionTextView.visibility = View.GONE
} }
@ -337,7 +326,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
} }
fun submitGuestChange() { 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) { if ((allowGuestsAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
ncApi.makeRoomPublic(conversationUser.getCredentials(), ApiUtils.getUrlForRoomVisibility ncApi.makeRoomPublic(conversationUser.getCredentials(), ApiUtils.getUrlForRoomVisibility
(conversationUser.baseUrl, conversation!!.token)) (conversationUser.baseUrl, conversation!!.token))
@ -379,7 +368,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
} }
fun submitFavoriteChange() { 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) { if ((favoriteConversationAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
ncApi.addConversationToFavorites(conversationUser.getCredentials(), ApiUtils ncApi.addConversationToFavorites(conversationUser.getCredentials(), ApiUtils
.getUrlForConversationFavorites(conversationUser.baseUrl, conversation!!.token)) .getUrlForConversationFavorites(conversationUser.baseUrl, conversation!!.token))

View File

@ -23,16 +23,38 @@
package com.nextcloud.talk.newarch.data.repository.offline package com.nextcloud.talk.newarch.data.repository.offline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
import com.nextcloud.talk.newarch.local.dao.MessagesDao 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( override fun getMessagesWithUserForConversation(
conversationId: String conversationId: String
): LiveData<List<ChatMessage>> { ): LiveData<List<ChatMessage>> {
TODO( return messagesDao.getMessagesWithUserForConversation(conversationId).distinctUntilChanged().map {
"not implemented" it.map { messageEntity ->
) //To change body of created functions use File | Settings | File Templates. 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 package com.nextcloud.talk.newarch.data.repository.online
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall 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.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationOverall import com.nextcloud.talk.models.json.conversations.ConversationOverall
import com.nextcloud.talk.models.json.generic.GenericOverall 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.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiService import com.nextcloud.talk.newarch.data.source.remote.ApiService
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository 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.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import retrofit2.Response
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository { class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
override suspend fun deleteConversationForUser( 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 { override suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall {
return apiService.getNotification(user.getCredentials(), ApiUtils.getUrlForNotificationWithId(user.baseUrl, notificationId)) 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.autocomplete.AutocompleteOverall
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall 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.ConversationOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall 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.push.PushRegistrationOverall
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import io.reactivex.Observable
import retrofit2.Response
import retrofit2.http.* import retrofit2.http.*
interface ApiService { 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 @GET
suspend fun getPeersForCall(@Header("Authorization") authorization: String, suspend fun getPeersForCall(@Header("Authorization") authorization: String,
@Url url: String): ParticipantsOverall @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.domain.usecases.*
import com.nextcloud.talk.newarch.features.chat.ChatViewModelFactory import com.nextcloud.talk.newarch.features.chat.ChatViewModelFactory
import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.utils.NetworkComponents
import org.koin.dsl.module import org.koin.dsl.module
val UseCasesModule = module { val UseCasesModule = module {
single { createGetConversationUseCase(get(), get()) } factory { createGetConversationUseCase(get(), get()) }
single { createGetConversationsUseCase(get(), get()) } factory { createGetConversationsUseCase(get(), get()) }
single { createSetConversationFavoriteValueUseCase(get(), get()) } factory { createSetConversationFavoriteValueUseCase(get(), get()) }
single { createLeaveConversationUseCase(get(), get()) } factory { createLeaveConversationUseCase(get(), get()) }
single { createDeleteConversationUseCase(get(), get()) } factory { createDeleteConversationUseCase(get(), get()) }
single { createJoinConversationUseCase(get(), get()) } factory { createJoinConversationUseCase(get(), get()) }
single { createExitConversationUseCase(get(), get()) } factory { createExitConversationUseCase(get(), get()) }
single { createGetProfileUseCase(get(), get()) } factory { createGetProfileUseCase(get(), get()) }
single { createGetSignalingUseCase(get(), get()) } factory { createGetSignalingUseCase(get(), get()) }
single { createGetCapabilitiesUseCase(get(), get()) } factory { createGetCapabilitiesUseCase(get(), get()) }
single { createRegisterPushWithProxyUseCase(get(), get()) } factory { createRegisterPushWithProxyUseCase(get(), get()) }
single { createRegisterPushWithServerUseCase(get(), get()) } factory { createRegisterPushWithServerUseCase(get(), get()) }
single { createUnregisterPushWithProxyUseCase(get(), get()) } factory { createUnregisterPushWithProxyUseCase(get(), get()) }
single { createUnregisterPushWithServerUseCase(get(), get()) } factory { createUnregisterPushWithServerUseCase(get(), get()) }
single { createGetContactsUseCase(get(), get()) } factory { createGetContactsUseCase(get(), get()) }
single { createCreateConversationUseCase(get(), get()) } factory { createCreateConversationUseCase(get(), get()) }
single { createAddParticipantToConversationUseCase(get(), get()) } factory { createAddParticipantToConversationUseCase(get(), get()) }
single { setConversationPasswordUseCase(get(), get()) } factory { setConversationPasswordUseCase(get(), get()) }
factory { getParticipantsForCallUseCase(get(), get()) } factory { getParticipantsForCallUseCase(get(), get()) }
factory { createGetChatMessagesUseCase(get(), get()) }
factory { getNotificationUseCase(get(), get()) } factory { getNotificationUseCase(get(), get()) }
factory { createChatViewModelFactory(get(), get(), get(), get(), 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, fun getNotificationUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler): GetNotificationUseCase { apiErrorHandler: ApiErrorHandler): GetNotificationUseCase {
return GetNotificationUseCase(nextcloudTalkRepository, apiErrorHandler) return GetNotificationUseCase(nextcloudTalkRepository, apiErrorHandler)
@ -181,6 +187,6 @@ fun createExitConversationUseCase(nextcloudTalkRepository: NextcloudTalkReposito
return ExitConversationUseCase(nextcloudTalkRepository, apiErrorHandler) return ExitConversationUseCase(nextcloudTalkRepository, apiErrorHandler)
} }
fun createChatViewModelFactory(application: Application, joinConversationUseCase: JoinConversationUseCase, exitConversationUseCase: ExitConversationUseCase, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory { fun createChatViewModelFactory(application: Application, networkComponents: NetworkComponents, apiErrorHandler: ApiErrorHandler, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory {
return ChatViewModelFactory(application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService) return ChatViewModelFactory(application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService)
} }

View File

@ -27,5 +27,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage
interface MessagesRepository { interface MessagesRepository {
fun getMessagesWithUserForConversation(conversationId: String): LiveData<List<ChatMessage>> 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 package com.nextcloud.talk.newarch.domain.repository.online
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall 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.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationOverall import com.nextcloud.talk.models.json.conversations.ConversationOverall
import com.nextcloud.talk.models.json.generic.GenericOverall 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.push.PushRegistrationOverall
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall 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 com.nextcloud.talk.newarch.local.models.UserNgEntity
import retrofit2.Response
interface NextcloudTalkRepository { 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 getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall
suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall
suspend fun setPasswordForConversation(user: UserNgEntity, conversationToken: String, password: String): GenericOverall 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. // Store the last header that was added, even if it belongs to a previous page.
private var headersAlreadyAdded = mutableListOf<String>() 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 { override fun getElementType(data: Data<ChatElement, String>): Int {
return elementType return elementType

View File

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

View File

@ -1,11 +1,8 @@
package com.nextcloud.talk.newarch.features.chat package com.nextcloud.talk.newarch.features.chat
enum class ChatElementTypes { enum class ChatElementTypes {
INCOMING_TEXT_MESSAGE,
OUTGOING_TEXT_MESSAGE,
INCOMING_PREVIEW_MESSAGE,
OUTGOING_PREVIEW_MESSAGE,
SYSTEM_MESSAGE, SYSTEM_MESSAGE,
UNREAD_MESSAGE_NOTICE, 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.otaliastudios.elements.extensions.HeaderSource
import com.stfalcon.chatkit.utils.DateFormatter import com.stfalcon.chatkit.utils.DateFormatter
import kotlinx.android.synthetic.main.item_message_quote.view.* 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_item.view.*
import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.*
import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.messageUserAvatar
import kotlinx.android.synthetic.main.rv_chat_outgoing_preview_item.view.*
import kotlinx.android.synthetic.main.rv_chat_outgoing_text_item.view.*
import kotlinx.android.synthetic.main.rv_chat_system_item.view.* import kotlinx.android.synthetic.main.rv_chat_system_item.view.*
import kotlinx.android.synthetic.main.rv_date_and_unread_notice_item.view.* import kotlinx.android.synthetic.main.rv_date_and_unread_notice_item.view.*
import org.koin.core.KoinComponent 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> 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 { override fun onCreate(parent: ViewGroup, elementType: Int): Holder {
return when (elementType) { return when (elementType) {
ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal -> { ChatElementTypes.CHAT_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_chat_incoming_text_item, parent, false)) Holder(getLayoutInflater().inflate(R.layout.rv_chat_item, parent, false))
}
ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_chat_outgoing_text_item, parent, false))
}
ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false))
}
ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false))
} }
ChatElementTypes.SYSTEM_MESSAGE.ordinal -> { ChatElementTypes.SYSTEM_MESSAGE.ordinal -> {
Holder(getLayoutInflater().inflate(R.layout.rv_chat_system_item, parent, false)) 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) super.onBind(page, holder, element, payloads)
holder.itemView.setOnLongClickListener { holder.itemView.setOnLongClickListener {
onElementLongClick?.invoke(page, holder, element) onElementLongClick?.invoke(page, holder, element, mapOf())
true true
} }
var chatElement: ChatElement? var chatElement: ChatElement? = null
var chatMessage: ChatMessage? = null var chatMessage: ChatMessage? = null
if (element.data is ChatElement) { if (element.data is ChatElement) {
@ -74,141 +61,117 @@ open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Hold
when { when {
chatMessage != null -> { chatMessage != null -> {
val elementType = chatElement!!.elementType
chatMessage.let { chatMessage.let {
if (element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal || element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal) { if (elementType == ChatElementTypes.CHAT_MESSAGE) {
holder.itemView.messageAuthor?.text = it.actorDisplayName holder.itemView.authorName?.text = it.actorDisplayName
holder.itemView.messageUserAvatar?.isVisible = !it.grouped && !it.oneToOneConversation 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) { if (it.actorType == "bots" && it.actorId == "changelog") {
holder.itemView.incomingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) val layers = arrayOfNulls<Drawable>(2)
holder.itemView.incomingMessageText.text = it.text layers[0] = context.getDrawable(R.drawable.ic_launcher_background)
layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground)
if (it.actorType == "bots" && it.actorId == "changelog") { val layerDrawable = LayerDrawable(layers)
holder.itemView.messageUserAvatar.isVisible = true val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable))
val layers = arrayOfNulls<Drawable>(2) imageLoader.getImageLoader().load(loadBuilder.build())
layers[0] = context.getDrawable(R.drawable.ic_launcher_background) } else if (it.actorType == "bots") {
layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground) val drawable = TextDrawable.builder()
val layerDrawable = LayerDrawable(layers) .beginConfig()
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable)) .bold()
imageLoader.getImageLoader().load(loadBuilder.build()) .endConfig()
} else if (it.actorType == "bots") { .buildRound(
holder.itemView.messageUserAvatar.isVisible = true ">",
val drawable = TextDrawable.builder() context.resources.getColor(R.color.black)
.beginConfig() )
.bold() val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(drawable))
.endConfig() imageLoader.getImageLoader().load(loadBuilder.build())
.buildRound(
">",
context.resources.getColor(R.color.black)
)
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(drawable))
imageLoader.getImageLoader().load(loadBuilder.build())
} else if (!it.grouped && !it.oneToOneConversation) {
holder.itemView.messageUserAvatar.isVisible = true
imageLoader.loadImage(holder.itemView.messageUserAvatar, it.user.avatar)
} else {
holder.itemView.messageUserAvatar.isVisible = false
}
} else {
holder.itemView.outgoingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
holder.itemView.outgoingMessageText.text = it.text
}
it.parentMessage?.let { parentMessage ->
parentMessage.imageUrl?.let { previewMessageUrl ->
holder.itemView.quotedMessageImage.visibility = View.VISIBLE
imageLoader.loadImage(holder.itemView.quotedMessageImage, previewMessageUrl)
} ?: run {
holder.itemView.quotedMessageImage.visibility = View.GONE
}
holder.itemView.quotedMessageAuthor.text = parentMessage.actorDisplayName ?: context.getText(R.string.nc_nick_guest)
holder.itemView.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.colorPrimary))
holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
holder.itemView.quotedChatMessageView.visibility = View.VISIBLE
} ?: run {
holder.itemView.quotedChatMessageView.visibility = View.GONE
}
} else if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) {
var previewAvailable = true
val mutableMap = mutableMapOf<String, String>()
if (it.selectedIndividualHashMap!!.containsKey("mimetype")) {
mutableMap.put("mimetype", it.selectedIndividualHashMap!!["mimetype"]!!)
if (it.imageUrl == "no-preview") {
previewAvailable = false
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(chatMessage.selectedIndividualHashMap!!["mimetype"]))
}
}
// Before someone tells me parts of this can be refactored so there is less code:
// YES, I KNOW!
// But the way it's done now means pretty much anyone can understand it and it's easy
// to modify. Prefer simplicity over complexity wherever possible
if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal) {
if (previewAvailable) {
imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!)
}
if (!it.grouped && !it.oneToOneConversation) {
holder.itemView.messageUserAvatar.visibility = View.GONE
} else {
holder.itemView.messageUserAvatar.visibility = View.VISIBLE
imageLoader.loadImage(holder.itemView.messageUserAvatar, chatMessage.user.avatar)
}
when (it.messageType) {
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
holder.itemView.incomingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"]
}
ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> {
holder.itemView.incomingPreviewMessageText.text = "GIPHY"
}
ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> {
holder.itemView.incomingPreviewMessageText.text = "TENOR"
}
else -> {
holder.itemView.incomingPreviewMessageText.text = ""
}
}
holder.itemView.incomingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
} else {
if (previewAvailable) {
imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!)
}
when (it.messageType) {
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
holder.itemView.outgoingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"]
}
ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> {
holder.itemView.outgoingPreviewMessageText.text = "GIPHY"
}
ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> {
holder.itemView.outgoingPreviewMessageText.text = "TENOR"
}
else -> {
holder.itemView.outgoingPreviewMessageText.text = ""
}
}
holder.itemView.outgoingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
}
} else { } else {
// it's ChatElementTypes.SYSTEM_MESSAGE imageLoader.loadImage(holder.itemView.authorAvatar, it.user.avatar)
holder.itemView.systemMessageText.text = chatMessage.text
holder.itemView.systemItemTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
} }
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) element.type == ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal -> {
} holder.itemView.noticeText.text = context.resources.getString(R.string.nc_new_messages)
else -> { }
// Date header else -> {
holder.itemView.noticeText.text = (element.data as HeaderSource.Data<*, *>).header.toString() // 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 android.widget.ImageView
import androidx.lifecycle.observe import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import coil.api.load import coil.api.load
import coil.target.Target import coil.target.Target
@ -71,8 +72,6 @@ import com.otaliastudios.elements.Adapter
import com.otaliastudios.elements.Element import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Presenter import com.otaliastudios.elements.Presenter
import com.otaliastudios.elements.pagers.PageSizePager
import com.stfalcon.chatkit.messages.MessagesListAdapter
import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import kotlinx.android.synthetic.main.controller_chat.view.* import kotlinx.android.synthetic.main.controller_chat.view.*
import kotlinx.android.synthetic.main.lobby_view.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 conversationVoiceCallMenuItem: MenuItem? = null
var conversationVideoMenuItem: MenuItem? = null var conversationVideoMenuItem: MenuItem? = null
private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage>
private lateinit var mentionAutocomplete: Autocomplete<*> private lateinit var mentionAutocomplete: Autocomplete<*>
private var shouldShowLobby: Boolean = false private var shouldShowLobby: Boolean = false
@ -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)) viewModel.init(bundle.getParcelable(BundleKeys.KEY_USER)!!, bundle.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, bundle.getString(KEY_CONVERSATION_PASSWORD))
messagesAdapter = Adapter.builder(this) messagesAdapter = Adapter.builder(this)
.setPager(PageSizePager(80)) .addSource(ChatViewLiveDataSource(viewModel.messagesLiveData))
//.addSource(ChatViewSource(itemsPerPage = 10))
.addSource(ChatDateHeaderSource(activity as Context, ChatElementTypes.DATE_HEADER.ordinal)) .addSource(ChatDateHeaderSource(activity as Context, ChatElementTypes.DATE_HEADER.ordinal))
.addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state)) .addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state))
.addPresenter(ChatPresenter(activity as Context, ::onElementClick, ::onElementLongClick, this)) .addPresenter(ChatPresenter(activity as Context, ::onElementClick, ::onElementLongClick, this))
.setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true)
.into(view.messagesRecyclerView) .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 { viewModel.apply {
conversation.observe(this@ChatView) { conversation -> conversation.observe(this@ChatView) { conversation ->
setTitle() setTitle()
@ -151,7 +165,7 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
view.lobbyTextView?.setText(R.string.nc_lobby_waiting) view.lobbyTextView?.setText(R.string.nc_lobby_waiting)
} }
} else { } else {
view.messagesRecyclerView?.visibility = View.GONE view.messagesRecyclerView?.visibility = View.VISIBLE
view.lobbyView?.visibility = View.GONE view.lobbyView?.visibility = View.GONE
if (isReadOnlyConversation) { if (isReadOnlyConversation) {
@ -165,8 +179,8 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
return view return view
} }
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>) { private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>, payload: Map<String, String>) {
if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) { if (element.type == ChatElementTypes.CHAT_MESSAGE.ordinal) {
element.data?.let { chatElement -> element.data?.let { chatElement ->
val chatMessage = chatElement.data as ChatMessage val chatMessage = chatElement.data as ChatMessage
val currentUser = viewModel.user 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) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
viewModel.view = this
setupViews() setupViews()
} }
override fun onDetach(view: View) {
super.onDetach(view)
viewModel.view = null
}
override fun onCreateOptionsMenu( override fun onCreateOptionsMenu(
menu: Menu, menu: Menu,
inflater: MenuInflater inflater: MenuInflater
) { ) {
super.onCreateOptionsMenu(menu, inflater) 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) { override fun onPrepareOptionsMenu(menu: Menu) {
@ -258,14 +278,14 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
conversationVoiceCallMenuItem?.isVisible = true conversationVoiceCallMenuItem?.isVisible = true
conversationVideoMenuItem?.isVisible = true conversationVideoMenuItem?.isVisible = true
} }
if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == viewModel.conversation.value?.type) {
loadAvatar()
}
} }
private fun setupViews() { private fun setupViews() {
view?.let { view -> view?.let { view ->
view.messagesRecyclerView.initRecyclerView(
LinearLayoutManager(view.context), recyclerViewAdapter, false
)
view.popupBubbleView.setRecyclerView(view.messagesRecyclerView) view.popupBubbleView.setRecyclerView(view.messagesRecyclerView)
val filters = arrayOfNulls<InputFilter>(1) val filters = arrayOfNulls<InputFilter>(1)
@ -416,30 +436,32 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
private fun loadAvatar() { private fun loadAvatar() {
val imageLoader = networkComponents.getImageLoader(viewModel.user) val imageLoader = networkComponents.getImageLoader(viewModel.user)
val avatarSize = DisplayUtils.convertDpToPixel( conversationVoiceCallMenuItem?.let {
conversationVoiceCallMenuItem?.icon!! val avatarSize = DisplayUtils.convertDpToPixel(
.intrinsicWidth.toFloat(), activity!! it.icon!!.intrinsicWidth.toFloat(), activity!!
) )
.toInt() .toInt()
avatarSize.let { avatarSize.let {
val target = object : Target { val target = object : Target {
override fun onSuccess(result: Drawable) { override fun onSuccess(result: Drawable) {
super.onSuccess(result) super.onSuccess(result)
actionBar?.setIcon(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 package com.nextcloud.talk.newarch.features.chat
import android.app.Application import android.app.Application
import android.text.TextUtils
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope 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.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel 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.ConversationsRepository
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.GetChatMessagesUseCase
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.local.models.User import com.nextcloud.talk.newarch.local.models.User
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.toUser
import com.nextcloud.talk.newarch.local.models.toUserEntity
import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.services.GlobalServiceInterface import com.nextcloud.talk.newarch.services.GlobalServiceInterface
import com.nextcloud.talk.newarch.utils.NetworkComponents
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
import retrofit2.Response
class ChatViewModel constructor(application: Application, class ChatViewModel constructor(application: Application,
private val joinConversationUseCase: JoinConversationUseCase, private val networkComponents: NetworkComponents,
private val exitConversationUseCase: ExitConversationUseCase, private val apiErrorHandler: ApiErrorHandler,
private val conversationsRepository: ConversationsRepository, private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface { private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface {
lateinit var user: User lateinit var user: User
val conversation: MutableLiveData<Conversation?> = MutableLiveData() val conversation: MutableLiveData<Conversation?> = MutableLiveData()
var initConversation: Conversation? = null var pastStartingPoint: Long = -1
val messagesLiveData = Transformations.switchMap(conversation) { val futureStartingPoint: MutableLiveData<Long> = MutableLiveData()
it?.let { private var initConversation: Conversation? = null
messagesRepository.getMessagesWithUserForConversation(it.conversationId!!)
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 conversationPassword: String? = null
var view: Controller? = null
fun init(user: User, conversationToken: String, conversationPassword: String?) { 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) { override suspend fun gotConversationInfoForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) {
if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) { if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) {
if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) { if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) {
this.conversation.value = conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!) this.conversation.postValue(conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!))
conversation.token?.let { conversationToken -> conversation.token?.let { conversationToken ->
globalService.joinConversation(conversationToken, conversationPassword, this) 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) { 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 android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.ConversationsRepository
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase
import com.nextcloud.talk.newarch.services.GlobalService import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.utils.NetworkComponents
class ChatViewModelFactory constructor( class ChatViewModelFactory constructor(
private val application: Application, private val application: Application,
private val joinConversationUseCase: JoinConversationUseCase, private val networkComponents: NetworkComponents,
private val exitConversationUseCase: ExitConversationUseCase, private val apiErrorHandler: ApiErrorHandler,
private val conversationsRepository: ConversationsRepository, private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
private val globalService: GlobalService private val globalService: GlobalService
@ -42,7 +44,7 @@ class ChatViewModelFactory constructor(
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ChatViewModel( return ChatViewModel(
application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService
) as T ) 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.ParticipantElement
import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView import com.nextcloud.talk.newarch.features.contactsflow.groupconversation.GroupConversationView
import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher
import com.nextcloud.talk.newarch.local.models.toUser
import com.nextcloud.talk.newarch.mvvm.BaseView import com.nextcloud.talk.newarch.mvvm.BaseView
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.newarch.utils.ElementPayload import com.nextcloud.talk.newarch.utils.ElementPayload
@ -179,10 +180,13 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
ContactsViewOperationState.OK -> { ContactsViewOperationState.OK -> {
val bundle = Bundle() val bundle = Bundle()
if (!hasToken || isNewGroupConversation) { if (!hasToken || isNewGroupConversation) {
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken) globalService.currentUserLiveData.value?.let {
router.replaceTopController(RouterTransaction.with(ChatView(bundle)) bundle.putParcelable(BundleKeys.KEY_USER, it.toUser())
.popChangeHandler(HorizontalChangeHandler()) bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
.pushChangeHandler(HorizontalChangeHandler())) router.replaceTopController(RouterTransaction.with(ChatView(bundle))
.popChangeHandler(HorizontalChangeHandler())
.pushChangeHandler(HorizontalChangeHandler()))
}
} else { } else {
// we added the participants - go back to conversations info // we added the participants - go back to conversations info
router.popCurrentController() router.popCurrentController()

View File

@ -155,6 +155,7 @@ open class ConversationPresenter(context: Context, onElementClick: ((Page, Holde
addHeader("Authorization", user.getCredentials()) addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
fallback(Images().getImageForConversation(context, conversation, true)) 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 package com.nextcloud.talk.newarch.features.conversationsList
import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -82,7 +83,7 @@ class ConversationsListView : BaseView() {
val adapter = Adapter.builder(this) val adapter = Adapter.builder(this)
.addSource(ConversationsListSource(viewModel.conversationsLiveData)) .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(Presenter.forLoadingIndicator(context, R.layout.loading_state))
.addPresenter(AdvancedEmptyPresenter(context, R.layout.message_state, ::openNewConversationScreen) { view -> .addPresenter(AdvancedEmptyPresenter(context, R.layout.message_state, ::openNewConversationScreen) { view ->
view.messageStateImageView.imageTintList = resources?.getColor(R.color.colorPrimary)?.let { ColorStateList.valueOf(it) } view.messageStateImageView.imageTintList = resources?.getColor(R.color.colorPrimary)?.let { ColorStateList.valueOf(it) }
@ -163,7 +164,7 @@ class ConversationsListView : BaseView() {
conversation?.let { conversation -> conversation?.let { conversation ->
val bundle = Bundle() val bundle = Bundle()
with(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_CONVERSATION_TOKEN, conversation.token)
putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId) putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId)
putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation)) putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))

View File

@ -179,11 +179,13 @@ class ConversationsListViewModel (
operationUser?.let { operationUser?.let {
viewModelScope.launch { viewModelScope.launch {
val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 256) val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 256)
val drawable = Coil.get((url)) { try {
addHeader("Authorization", it.getCredentials()) val drawable = Coil.get((url)) {
transformations(CircleCropTransformation()) addHeader("Authorization", it.getCredentials())
} transformations(CircleCropTransformation())
avatar.postValue(drawable) }
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 timestamp: Long
) )
@Query("SELECT * FROM conversations where id = :internalUserId AND token = :token") @Query("SELECT * FROM conversations where user_id = :userId AND token = :token")
abstract suspend fun getConversationForUserWithToken(internalUserId: Long, token: String): ConversationEntity? abstract suspend fun getConversationForUserWithToken(userId: Long, token: String): ConversationEntity?
@Transaction @Transaction
open suspend fun updateConversationsForUser( open suspend fun updateConversationsForUser(

View File

@ -31,10 +31,13 @@ import com.nextcloud.talk.newarch.local.models.MessageEntity
@Dao @Dao
abstract class MessagesDao { 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): abstract fun getMessagesWithUserForConversation(conversationId: String):
LiveData<List<MessageEntity>> LiveData<List<MessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @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.ConversationEntity
import com.nextcloud.talk.newarch.local.models.MessageEntity import com.nextcloud.talk.newarch.local.models.MessageEntity
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
import org.parceler.converter.HashMapParcelConverter
@Database( @Database(
entities = [ConversationEntity::class, MessageEntity::class, UserNgEntity::class], entities = [ConversationEntity::class, MessageEntity::class, UserNgEntity::class],
@ -46,7 +47,8 @@ import com.nextcloud.talk.newarch.local.models.UserNgEntity
ConversationTypeConverter::class, ParticipantTypeConverter::class, ConversationTypeConverter::class, ParticipantTypeConverter::class,
PushConfigurationConverter::class, CapabilitiesConverter::class, PushConfigurationConverter::class, CapabilitiesConverter::class,
SignalingSettingsConverter::class, SignalingSettingsConverter::class,
UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class,
HashMapHashMapConverter::class
) )
abstract class TalkDatabase : RoomDatabase() { abstract class TalkDatabase : RoomDatabase() {

View File

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

View File

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

View File

@ -129,10 +129,16 @@ class ShortcutService constructor(private var context: Context,
iconImage = images.getImageForConversation(context, conversation) iconImage = images.getImageForConversation(context, conversation)
if (iconImage == null) { if (iconImage == null) {
iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) { try {
addHeader("Authorization", user.getCredentials()) iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) {
transformations(CircleCropTransformation()) 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)) shortcuts.add(ShortcutInfoCompat.Builder(context, "current_conversation_" + (index + 1))

View File

@ -47,6 +47,7 @@ class NetworkComponents(
val usersMultipleOperationsRepositoryMap: MutableMap<Long, NextcloudTalkRepository> = mutableMapOf() val usersMultipleOperationsRepositoryMap: MutableMap<Long, NextcloudTalkRepository> = mutableMapOf()
val usersSingleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf() val usersSingleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
val usersMultipleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf() val usersMultipleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
val usersImageLoaderMap: MutableMap<Long, ImageLoader> = mutableMapOf()
fun getRepository(singleOperation: Boolean, user: User): NextcloudTalkRepository { fun getRepository(singleOperation: Boolean, user: User): NextcloudTalkRepository {
val mappedNextcloudTalkRepository = if (singleOperation) { val mappedNextcloudTalkRepository = if (singleOperation) {
@ -89,20 +90,28 @@ class NetworkComponents(
} }
fun getImageLoader(user: User): ImageLoader { fun getImageLoader(user: User): ImageLoader {
return ImageLoader(androidApplication) { var mappedImageLoader = usersImageLoaderMap[user.id]
availableMemoryPercentage(0.5)
bitmapPoolPercentage(0.5) if (mappedImageLoader == null) {
crossfade(false) mappedImageLoader = ImageLoader(androidApplication) {
okHttpClient(getOkHttpClient(false, user)) availableMemoryPercentage(0.5)
componentRegistry { bitmapPoolPercentage(0.5)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { crossfade(false)
add(ImageDecoderDecoder()) okHttpClient(getOkHttpClient(false, user))
} else { componentRegistry {
add(GifDecoder()) 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(); .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) { public Observable deleteAllEntriesForAccountIdentifier(long accountIdentifier) {
ReactiveScalar<Integer> deleteResult = dataStore.delete(ArbitraryStorage.class) 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_above="@+id/separator" android:layout_above="@+id/separator"
app:stackFromEnd="true"
app:reverseLayout="true"
android:id="@+id/messagesRecyclerView"/> android:id="@+id/messagesRecyclerView"/>
<View <View

View File

@ -1,77 +1,87 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <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" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_marginBottom="4dp">
<View <View
android:id="@+id/quoteColoredView" android:id="@+id/quoteColoredView"
android:layout_width="2dp" android:layout_width="2dp"
android:layout_height="match_parent" android:layout_height="100dp"
android:layout_alignBottom="@id/flexboxQuoted"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignBottom="@id/quotedChatText"
android:layout_marginEnd="4dp" 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 <androidx.emoji.widget.EmojiTextView
android:id="@+id/quotedMessageAuthor" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:layout_marginHorizontal="8dp"
android:layout_marginEnd="8dp" android:layout_marginTop="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_toStartOf="@id/cancelReplyButton" android:layout_toStartOf="@id/cancelReplyButton"
android:layout_marginTop="4dp" android:id="@+id/quotedChatText"
android:orientation="vertical" android:layout_below="@id/quotedPreviewImage"
app:alignContent="stretch" tools:text="Just another chat message"/>
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_start">
<ImageView <TextView
android:id="@+id/quotedMessageImage" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_alignParentEnd="true"
android:adjustViewBounds="true" android:layout_below="@id/quotedChatText"
android:scaleType="fitCenter" android:textSize="12sp"
app:layout_alignSelf="flex_start" android:id="@+id/quotedMessageTime"
app:layout_flexGrow="1" android:layout_marginEnd="8dp"
app:layout_wrapBefore="true" tools:text="12:30"/>
tools:src="@tools:sample/backgrounds/scenic" />
<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 <ImageButton
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
android:layout_marginStart="8dp" android:layout_marginHorizontal="8dp"
android:layout_marginEnd="8dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:background="@drawable/ic_cancel_black_24dp" 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"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <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 <TextView
android:id="@+id/noticeText" android:id="@+id/noticeText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textAlignment="center"
android:padding="16dp"/> android:padding="16dp"/>
</RelativeLayout> </RelativeLayout>

View File

@ -36,7 +36,7 @@ buildscript {
} }
} }
dependencies { 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-gradle-plugin:${kotlin_version}"
classpath "org.jetbrains.kotlin:kotlin-serialization:$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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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