mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-21 20:49:36 +01:00
Working offline chat
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
6ea115a4f9
commit
16102774f5
@ -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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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))
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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()
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>>?
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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>>
|
||||||
}
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
|
@ -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>
|
|
77
app/src/main/res/layout/rv_chat_item.xml
Normal file
77
app/src/main/res/layout/rv_chat_item.xml
Normal 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>
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
@ -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"
|
||||||
|
|
||||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user