mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-21 12:39:58 +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,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "4976b952409bfae25e1f9bc8df18c11c",
|
||||
"identityHash": "4e8c1ae6a440d8491937afe33a3ab085",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "conversations",
|
||||
@ -212,7 +212,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "messages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation_id` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `messageParameters` TEXT, `parent` TEXT, `replyable` INTEGER NOT NULL, `system_message_type` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@ -262,6 +262,18 @@
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "messageParameters",
|
||||
"columnName": "messageParameters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentMessage",
|
||||
"columnName": "parent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replyable",
|
||||
"columnName": "replyable",
|
||||
@ -389,7 +401,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4976b952409bfae25e1f9bc8df18c11c')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4e8c1ae6a440d8491937afe33a3ab085')"
|
||||
]
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryView
|
||||
import com.nextcloud.talk.newarch.features.contactsflow.contacts.ContactsView
|
||||
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListView
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import com.nextcloud.talk.newarch.local.models.toUser
|
||||
import com.nextcloud.talk.utils.ConductorRemapping
|
||||
import com.nextcloud.talk.utils.SecurityUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
@ -147,7 +148,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
|
||||
// due to complications with persistablebundle not supporting complex types we do this magic
|
||||
// remove this once we rewrite chat magic
|
||||
val extras = intent.extras!!
|
||||
extras.putParcelable(BundleKeys.KEY_USER_ENTITY, it)
|
||||
extras.putParcelable(BundleKeys.KEY_USER, it.toUser())
|
||||
withContext(Dispatchers.Main) {
|
||||
ConductorRemapping.remapChatController(
|
||||
router!!, it.id,
|
||||
|
@ -76,7 +76,6 @@ import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.ShareUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
|
||||
import com.nextcloud.talk.utils.ui.MaterialPreferenceCategoryWithRightLink
|
||||
import com.yarolegovich.lovelydialog.LovelySaveStateHandler
|
||||
import com.yarolegovich.lovelydialog.LovelyStandardDialog
|
||||
@ -190,7 +189,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
|
||||
private var roomDisposable: Disposable? = null
|
||||
private var participantsDisposable: Disposable? = null
|
||||
|
||||
private var databaseStorageModule: DatabaseStorageModule? = null
|
||||
private var conversation: Conversation? = null
|
||||
|
||||
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
|
||||
@ -253,15 +251,6 @@ class ConversationInfoController(args: Bundle) : BaseController(),
|
||||
saveStateHandler = LovelySaveStateHandler()
|
||||
}
|
||||
|
||||
if (databaseStorageModule == null) {
|
||||
databaseStorageModule = DatabaseStorageModule(
|
||||
conversationUser!!, conversationToken!!, this)
|
||||
}
|
||||
|
||||
notificationsPreferenceScreen.setStorageModule(databaseStorageModule)
|
||||
conversationInfoWebinar.setStorageModule(databaseStorageModule)
|
||||
generalConversationOptions.setStorageModule(databaseStorageModule)
|
||||
|
||||
actionTextView.visibility = View.GONE
|
||||
}
|
||||
|
||||
@ -337,7 +326,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
|
||||
}
|
||||
|
||||
fun submitGuestChange() {
|
||||
if (databaseStorageModule != null && conversationUser != null && conversation != null) {
|
||||
if ( conversationUser != null && conversation != null) {
|
||||
if ((allowGuestsAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
|
||||
ncApi.makeRoomPublic(conversationUser.getCredentials(), ApiUtils.getUrlForRoomVisibility
|
||||
(conversationUser.baseUrl, conversation!!.token))
|
||||
@ -379,7 +368,7 @@ class ConversationInfoController(args: Bundle) : BaseController(),
|
||||
}
|
||||
|
||||
fun submitFavoriteChange() {
|
||||
if (databaseStorageModule != null && conversationUser != null && conversation != null) {
|
||||
if (conversationUser != null && conversation != null) {
|
||||
if ((favoriteConversationAction.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked) {
|
||||
ncApi.addConversationToFavorites(conversationUser.getCredentials(), ApiUtils
|
||||
.getUrlForConversationFavorites(conversationUser.baseUrl, conversation!!.token))
|
||||
|
@ -23,16 +23,38 @@
|
||||
package com.nextcloud.talk.newarch.data.repository.offline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
|
||||
import com.nextcloud.talk.newarch.local.dao.MessagesDao
|
||||
import com.nextcloud.talk.newarch.local.models.toChatMessage
|
||||
import com.nextcloud.talk.newarch.local.models.toMessageEntity
|
||||
|
||||
class MessagesRepositoryImpl(val messagesDao: MessagesDao) : MessagesRepository {
|
||||
class MessagesRepositoryImpl(private val messagesDao: MessagesDao) : MessagesRepository {
|
||||
override fun getMessagesWithUserForConversation(
|
||||
conversationId: String
|
||||
): LiveData<List<ChatMessage>> {
|
||||
TODO(
|
||||
"not implemented"
|
||||
) //To change body of created functions use File | Settings | File Templates.
|
||||
return messagesDao.getMessagesWithUserForConversation(conversationId).distinctUntilChanged().map {
|
||||
it.map { messageEntity ->
|
||||
messageEntity.toChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<ChatMessage>> {
|
||||
return messagesDao.getMessagesWithUserForConversationSince(conversationId, messageId).distinctUntilChanged().map {
|
||||
it.map { messageEntity ->
|
||||
messageEntity.toChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveMessagesForConversation(messages: List<ChatMessage>): List<Long> {
|
||||
val updatedMessages = messages.map {
|
||||
it.toMessageEntity()
|
||||
}
|
||||
|
||||
return messagesDao.saveMessages(*updatedMessages.toTypedArray())
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
package com.nextcloud.talk.newarch.data.repository.online
|
||||
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall
|
||||
import com.nextcloud.talk.models.json.conversations.Conversation
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationOverall
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
@ -35,9 +36,11 @@ import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOveral
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
|
||||
import com.nextcloud.talk.newarch.data.source.remote.ApiService
|
||||
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
|
||||
import com.nextcloud.talk.newarch.local.models.User
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import com.nextcloud.talk.newarch.local.models.getCredentials
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import retrofit2.Response
|
||||
|
||||
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
|
||||
override suspend fun deleteConversationForUser(
|
||||
@ -95,6 +98,17 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int): Response<ChatOverall> {
|
||||
val mutableMap = mutableMapOf<String, Int>()
|
||||
mutableMap["lookIntoFuture"] = lookIntoFuture
|
||||
mutableMap["lastKnownMessageId"] = lastKnownMessageId
|
||||
mutableMap["includeLastKnown"] = includeLastKnown
|
||||
mutableMap["timeout"] = 30
|
||||
mutableMap["setReadMarker"] = 1
|
||||
|
||||
return apiService.pullChatMessages(user.getCredentials(), ApiUtils.getUrlForChat(user.baseUrl, conversationToken), mutableMap)
|
||||
}
|
||||
|
||||
override suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall {
|
||||
return apiService.getNotification(user.getCredentials(), ApiUtils.getUrlForNotificationWithId(user.baseUrl, notificationId))
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ package com.nextcloud.talk.newarch.data.source.remote
|
||||
|
||||
import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationOverall
|
||||
import com.nextcloud.talk.models.json.conversations.RoomsOverall
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
@ -33,9 +34,26 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
|
||||
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
|
||||
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
|
||||
interface ApiService {
|
||||
/*
|
||||
QueryMap items are as follows:
|
||||
- "lookIntoFuture": int (0 or 1),
|
||||
- "limit" : int, range 100-200,
|
||||
- "timeout": used with look into future, 30 default, 60 at most
|
||||
- "lastKnownMessageId", int, use one from X-Chat-Last-Given
|
||||
- "setReadMarker", int, default 1
|
||||
- "includeLastKnown", int, default 0
|
||||
*/
|
||||
|
||||
@GET
|
||||
suspend fun pullChatMessages(@Header("Authorization") authorization: String,
|
||||
@Url url: String,
|
||||
@QueryMap fields: Map<String, Int>): Response<ChatOverall>
|
||||
|
||||
@GET
|
||||
suspend fun getPeersForCall(@Header("Authorization") authorization: String,
|
||||
@Url url: String): ParticipantsOverall
|
||||
|
@ -30,32 +30,38 @@ import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkReposito
|
||||
import com.nextcloud.talk.newarch.domain.usecases.*
|
||||
import com.nextcloud.talk.newarch.features.chat.ChatViewModelFactory
|
||||
import com.nextcloud.talk.newarch.services.GlobalService
|
||||
import com.nextcloud.talk.newarch.utils.NetworkComponents
|
||||
import org.koin.dsl.module
|
||||
|
||||
val UseCasesModule = module {
|
||||
single { createGetConversationUseCase(get(), get()) }
|
||||
single { createGetConversationsUseCase(get(), get()) }
|
||||
single { createSetConversationFavoriteValueUseCase(get(), get()) }
|
||||
single { createLeaveConversationUseCase(get(), get()) }
|
||||
single { createDeleteConversationUseCase(get(), get()) }
|
||||
single { createJoinConversationUseCase(get(), get()) }
|
||||
single { createExitConversationUseCase(get(), get()) }
|
||||
single { createGetProfileUseCase(get(), get()) }
|
||||
single { createGetSignalingUseCase(get(), get()) }
|
||||
single { createGetCapabilitiesUseCase(get(), get()) }
|
||||
single { createRegisterPushWithProxyUseCase(get(), get()) }
|
||||
single { createRegisterPushWithServerUseCase(get(), get()) }
|
||||
single { createUnregisterPushWithProxyUseCase(get(), get()) }
|
||||
single { createUnregisterPushWithServerUseCase(get(), get()) }
|
||||
single { createGetContactsUseCase(get(), get()) }
|
||||
single { createCreateConversationUseCase(get(), get()) }
|
||||
single { createAddParticipantToConversationUseCase(get(), get()) }
|
||||
single { setConversationPasswordUseCase(get(), get()) }
|
||||
factory { createGetConversationUseCase(get(), get()) }
|
||||
factory { createGetConversationsUseCase(get(), get()) }
|
||||
factory { createSetConversationFavoriteValueUseCase(get(), get()) }
|
||||
factory { createLeaveConversationUseCase(get(), get()) }
|
||||
factory { createDeleteConversationUseCase(get(), get()) }
|
||||
factory { createJoinConversationUseCase(get(), get()) }
|
||||
factory { createExitConversationUseCase(get(), get()) }
|
||||
factory { createGetProfileUseCase(get(), get()) }
|
||||
factory { createGetSignalingUseCase(get(), get()) }
|
||||
factory { createGetCapabilitiesUseCase(get(), get()) }
|
||||
factory { createRegisterPushWithProxyUseCase(get(), get()) }
|
||||
factory { createRegisterPushWithServerUseCase(get(), get()) }
|
||||
factory { createUnregisterPushWithProxyUseCase(get(), get()) }
|
||||
factory { createUnregisterPushWithServerUseCase(get(), get()) }
|
||||
factory { createGetContactsUseCase(get(), get()) }
|
||||
factory { createCreateConversationUseCase(get(), get()) }
|
||||
factory { createAddParticipantToConversationUseCase(get(), get()) }
|
||||
factory { setConversationPasswordUseCase(get(), get()) }
|
||||
factory { getParticipantsForCallUseCase(get(), get()) }
|
||||
factory { createGetChatMessagesUseCase(get(), get()) }
|
||||
factory { getNotificationUseCase(get(), get()) }
|
||||
factory { createChatViewModelFactory(get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
|
||||
fun createGetChatMessagesUseCase(nextcloudTalkRepository: NextcloudTalkRepository, apiErrorHandler: ApiErrorHandler): GetChatMessagesUseCase {
|
||||
return GetChatMessagesUseCase(nextcloudTalkRepository, apiErrorHandler)
|
||||
}
|
||||
|
||||
fun getNotificationUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
|
||||
apiErrorHandler: ApiErrorHandler): GetNotificationUseCase {
|
||||
return GetNotificationUseCase(nextcloudTalkRepository, apiErrorHandler)
|
||||
@ -181,6 +187,6 @@ fun createExitConversationUseCase(nextcloudTalkRepository: NextcloudTalkReposito
|
||||
return ExitConversationUseCase(nextcloudTalkRepository, apiErrorHandler)
|
||||
}
|
||||
|
||||
fun createChatViewModelFactory(application: Application, joinConversationUseCase: JoinConversationUseCase, exitConversationUseCase: ExitConversationUseCase, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory {
|
||||
return ChatViewModelFactory(application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService)
|
||||
fun createChatViewModelFactory(application: Application, networkComponents: NetworkComponents, apiErrorHandler: ApiErrorHandler, conversationsRepository: ConversationsRepository, messagesRepository: MessagesRepository, globalService: GlobalService): ChatViewModelFactory {
|
||||
return ChatViewModelFactory(application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService)
|
||||
}
|
||||
|
@ -27,5 +27,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||
|
||||
interface MessagesRepository {
|
||||
fun getMessagesWithUserForConversation(conversationId: String): LiveData<List<ChatMessage>>
|
||||
|
||||
fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<ChatMessage>>
|
||||
suspend fun saveMessagesForConversation(messages: List<ChatMessage>): List<Long>
|
||||
}
|
@ -23,6 +23,7 @@
|
||||
package com.nextcloud.talk.newarch.domain.repository.online
|
||||
|
||||
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall
|
||||
import com.nextcloud.talk.models.json.conversations.Conversation
|
||||
import com.nextcloud.talk.models.json.conversations.ConversationOverall
|
||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
||||
@ -33,9 +34,12 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
|
||||
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
|
||||
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
|
||||
import com.nextcloud.talk.newarch.local.models.User
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import retrofit2.Response
|
||||
|
||||
interface NextcloudTalkRepository {
|
||||
suspend fun getChatMessagesForConversation(user: User, conversationToken: String, lookIntoFuture: Int, lastKnownMessageId: Int, includeLastKnown: Int = 0): Response<ChatOverall>
|
||||
suspend fun getNotificationForUser(user: UserNgEntity, notificationId: String): NotificationOverall
|
||||
suspend fun getParticipantsForCall(user: UserNgEntity, conversationToken: String): ParticipantsOverall
|
||||
suspend fun setPasswordForConversation(user: UserNgEntity, conversationToken: String, password: String): GenericOverall
|
||||
|
@ -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.
|
||||
private var headersAlreadyAdded = mutableListOf<String>()
|
||||
|
||||
override fun dependsOn(source: Source<*>) = source is ChatViewSource
|
||||
override fun dependsOn(source: Source<*>) = source is ChatViewLiveDataSource
|
||||
|
||||
override fun getElementType(data: Data<ChatElement, String>): Int {
|
||||
return elementType
|
||||
|
@ -2,5 +2,5 @@ package com.nextcloud.talk.newarch.features.chat
|
||||
|
||||
data class ChatElement(
|
||||
val data: Any,
|
||||
val elementType: Int
|
||||
val elementType: ChatElementTypes
|
||||
)
|
@ -1,11 +1,8 @@
|
||||
package com.nextcloud.talk.newarch.features.chat
|
||||
|
||||
enum class ChatElementTypes {
|
||||
INCOMING_TEXT_MESSAGE,
|
||||
OUTGOING_TEXT_MESSAGE,
|
||||
INCOMING_PREVIEW_MESSAGE,
|
||||
OUTGOING_PREVIEW_MESSAGE,
|
||||
SYSTEM_MESSAGE,
|
||||
UNREAD_MESSAGE_NOTICE,
|
||||
DATE_HEADER
|
||||
DATE_HEADER,
|
||||
CHAT_MESSAGE
|
||||
}
|
@ -20,32 +20,19 @@ import com.otaliastudios.elements.Presenter
|
||||
import com.otaliastudios.elements.extensions.HeaderSource
|
||||
import com.stfalcon.chatkit.utils.DateFormatter
|
||||
import kotlinx.android.synthetic.main.item_message_quote.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_incoming_preview_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_incoming_text_item.view.messageUserAvatar
|
||||
import kotlinx.android.synthetic.main.rv_chat_outgoing_preview_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_outgoing_text_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_chat_system_item.view.*
|
||||
import kotlinx.android.synthetic.main.rv_date_and_unread_notice_item.view.*
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Holder, Element<T>) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element<T>) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter<T>(context, onElementClick), KoinComponent {
|
||||
open class ChatPresenter<T : Any>(context: Context, private val onElementClickPass: ((Page, Holder, Element<T>, Map<String, String>) -> Unit)?, private val onElementLongClick: ((Page, Holder, Element<T>, Map<String, String>) -> Unit)?, private val imageLoader: ImageLoaderInterface) : Presenter<T>(context), KoinComponent {
|
||||
override val elementTypes: Collection<Int>
|
||||
get() = listOf(ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal, ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal, ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal, ChatElementTypes.SYSTEM_MESSAGE.ordinal, ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal, ChatElementTypes.DATE_HEADER.ordinal)
|
||||
get() = listOf(ChatElementTypes.SYSTEM_MESSAGE.ordinal, ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal, ChatElementTypes.DATE_HEADER.ordinal, ChatElementTypes.CHAT_MESSAGE.ordinal)
|
||||
|
||||
override fun onCreate(parent: ViewGroup, elementType: Int): Holder {
|
||||
return when (elementType) {
|
||||
ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_chat_incoming_text_item, parent, false))
|
||||
}
|
||||
ChatElementTypes.OUTGOING_TEXT_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_chat_outgoing_text_item, parent, false))
|
||||
}
|
||||
ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false))
|
||||
}
|
||||
ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_date_and_unread_notice_item, parent, false))
|
||||
ChatElementTypes.CHAT_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_chat_item, parent, false))
|
||||
}
|
||||
ChatElementTypes.SYSTEM_MESSAGE.ordinal -> {
|
||||
Holder(getLayoutInflater().inflate(R.layout.rv_chat_system_item, parent, false))
|
||||
@ -60,11 +47,11 @@ open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Hold
|
||||
super.onBind(page, holder, element, payloads)
|
||||
|
||||
holder.itemView.setOnLongClickListener {
|
||||
onElementLongClick?.invoke(page, holder, element)
|
||||
onElementLongClick?.invoke(page, holder, element, mapOf())
|
||||
true
|
||||
}
|
||||
|
||||
var chatElement: ChatElement?
|
||||
var chatElement: ChatElement? = null
|
||||
var chatMessage: ChatMessage? = null
|
||||
|
||||
if (element.data is ChatElement) {
|
||||
@ -74,141 +61,117 @@ open class ChatPresenter<T : Any>(context: Context, onElementClick: ((Page, Hold
|
||||
|
||||
when {
|
||||
chatMessage != null -> {
|
||||
val elementType = chatElement!!.elementType
|
||||
chatMessage.let {
|
||||
if (element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal || element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal) {
|
||||
holder.itemView.messageAuthor?.text = it.actorDisplayName
|
||||
holder.itemView.messageUserAvatar?.isVisible = !it.grouped && !it.oneToOneConversation
|
||||
if (elementType == ChatElementTypes.CHAT_MESSAGE) {
|
||||
holder.itemView.authorName?.text = it.actorDisplayName
|
||||
holder.itemView.messageTime?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
holder.itemView.chatMessage.text = it.text
|
||||
|
||||
if (element.type == ChatElementTypes.INCOMING_TEXT_MESSAGE.ordinal) {
|
||||
holder.itemView.incomingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
holder.itemView.incomingMessageText.text = it.text
|
||||
|
||||
if (it.actorType == "bots" && it.actorId == "changelog") {
|
||||
holder.itemView.messageUserAvatar.isVisible = true
|
||||
val layers = arrayOfNulls<Drawable>(2)
|
||||
layers[0] = context.getDrawable(R.drawable.ic_launcher_background)
|
||||
layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground)
|
||||
val layerDrawable = LayerDrawable(layers)
|
||||
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable))
|
||||
imageLoader.getImageLoader().load(loadBuilder.build())
|
||||
} else if (it.actorType == "bots") {
|
||||
holder.itemView.messageUserAvatar.isVisible = true
|
||||
val drawable = TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRound(
|
||||
">",
|
||||
context.resources.getColor(R.color.black)
|
||||
)
|
||||
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.messageUserAvatar).data(DisplayUtils.getRoundedDrawable(drawable))
|
||||
imageLoader.getImageLoader().load(loadBuilder.build())
|
||||
} else if (!it.grouped && !it.oneToOneConversation) {
|
||||
holder.itemView.messageUserAvatar.isVisible = true
|
||||
imageLoader.loadImage(holder.itemView.messageUserAvatar, it.user.avatar)
|
||||
} else {
|
||||
holder.itemView.messageUserAvatar.isVisible = false
|
||||
}
|
||||
} else {
|
||||
holder.itemView.outgoingMessageTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
holder.itemView.outgoingMessageText.text = it.text
|
||||
}
|
||||
|
||||
it.parentMessage?.let { parentMessage ->
|
||||
parentMessage.imageUrl?.let { previewMessageUrl ->
|
||||
holder.itemView.quotedMessageImage.visibility = View.VISIBLE
|
||||
imageLoader.loadImage(holder.itemView.quotedMessageImage, previewMessageUrl)
|
||||
} ?: run {
|
||||
holder.itemView.quotedMessageImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
holder.itemView.quotedMessageAuthor.text = parentMessage.actorDisplayName ?: context.getText(R.string.nc_nick_guest)
|
||||
holder.itemView.quotedMessageAuthor.setTextColor(context.resources.getColor(R.color.colorPrimary))
|
||||
holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
|
||||
holder.itemView.quotedChatMessageView.visibility = View.VISIBLE
|
||||
} ?: run {
|
||||
holder.itemView.quotedChatMessageView.visibility = View.GONE
|
||||
}
|
||||
|
||||
} else if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) {
|
||||
var previewAvailable = true
|
||||
val mutableMap = mutableMapOf<String, String>()
|
||||
if (it.selectedIndividualHashMap!!.containsKey("mimetype")) {
|
||||
mutableMap.put("mimetype", it.selectedIndividualHashMap!!["mimetype"]!!)
|
||||
if (it.imageUrl == "no-preview") {
|
||||
previewAvailable = false
|
||||
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(chatMessage.selectedIndividualHashMap!!["mimetype"]))
|
||||
}
|
||||
}
|
||||
|
||||
// Before someone tells me parts of this can be refactored so there is less code:
|
||||
// YES, I KNOW!
|
||||
// But the way it's done now means pretty much anyone can understand it and it's easy
|
||||
// to modify. Prefer simplicity over complexity wherever possible
|
||||
|
||||
if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal) {
|
||||
if (previewAvailable) {
|
||||
imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!)
|
||||
}
|
||||
if (!it.grouped && !it.oneToOneConversation) {
|
||||
holder.itemView.messageUserAvatar.visibility = View.GONE
|
||||
} else {
|
||||
holder.itemView.messageUserAvatar.visibility = View.VISIBLE
|
||||
imageLoader.loadImage(holder.itemView.messageUserAvatar, chatMessage.user.avatar)
|
||||
}
|
||||
|
||||
when (it.messageType) {
|
||||
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
|
||||
holder.itemView.incomingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"]
|
||||
}
|
||||
ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> {
|
||||
holder.itemView.incomingPreviewMessageText.text = "GIPHY"
|
||||
}
|
||||
ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> {
|
||||
holder.itemView.incomingPreviewMessageText.text = "TENOR"
|
||||
}
|
||||
else -> {
|
||||
holder.itemView.incomingPreviewMessageText.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemView.incomingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
} else {
|
||||
if (previewAvailable) {
|
||||
imageLoader.loadImage(holder.itemView.incomingPreviewImage, it.imageUrl!!)
|
||||
}
|
||||
|
||||
when (it.messageType) {
|
||||
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
|
||||
holder.itemView.outgoingPreviewMessageText.text = chatMessage.selectedIndividualHashMap!!["name"]
|
||||
}
|
||||
ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE -> {
|
||||
holder.itemView.outgoingPreviewMessageText.text = "GIPHY"
|
||||
}
|
||||
ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE -> {
|
||||
holder.itemView.outgoingPreviewMessageText.text = "TENOR"
|
||||
}
|
||||
else -> {
|
||||
holder.itemView.outgoingPreviewMessageText.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemView.outgoingPreviewTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
}
|
||||
if (it.actorType == "bots" && it.actorId == "changelog") {
|
||||
val layers = arrayOfNulls<Drawable>(2)
|
||||
layers[0] = context.getDrawable(R.drawable.ic_launcher_background)
|
||||
layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground)
|
||||
val layerDrawable = LayerDrawable(layers)
|
||||
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(layerDrawable))
|
||||
imageLoader.getImageLoader().load(loadBuilder.build())
|
||||
} else if (it.actorType == "bots") {
|
||||
val drawable = TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRound(
|
||||
">",
|
||||
context.resources.getColor(R.color.black)
|
||||
)
|
||||
val loadBuilder = imageLoader.getImageLoader().newLoadBuilder(context).target(holder.itemView.authorAvatar).data(DisplayUtils.getRoundedDrawable(drawable))
|
||||
imageLoader.getImageLoader().load(loadBuilder.build())
|
||||
} else {
|
||||
// it's ChatElementTypes.SYSTEM_MESSAGE
|
||||
holder.itemView.systemMessageText.text = chatMessage.text
|
||||
holder.itemView.systemItemTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
imageLoader.loadImage(holder.itemView.authorAvatar, it.user.avatar)
|
||||
}
|
||||
|
||||
it.parentMessage?.let { parentMessage ->
|
||||
holder.itemView.quotedMessageLayout.isVisible = true
|
||||
holder.itemView.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
|
||||
holder.itemView.quotedPreviewImage.setOnClickListener {
|
||||
onElementClickPass?.invoke(page, holder, element, mapOf("parentMessage" to "yes"))
|
||||
true
|
||||
}
|
||||
|
||||
parentMessage.imageUrl?.let { previewMessageUrl ->
|
||||
if (previewMessageUrl == "no-preview") {
|
||||
|
||||
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
|
||||
holder.itemView.quotedPreviewImage.visibility = View.VISIBLE
|
||||
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(parentMessage.selectedIndividualHashMap!!["mimetype"])) {
|
||||
target(holder.itemView.previewImage)
|
||||
}
|
||||
} else {
|
||||
holder.itemView.quotedPreviewImage.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
holder.itemView.quotedPreviewImage.visibility = View.VISIBLE
|
||||
val mutableMap = mutableMapOf<String, String>()
|
||||
if (parentMessage.selectedIndividualHashMap?.containsKey("mimetype") == true) {
|
||||
mutableMap["mimetype"] = it.selectedIndividualHashMap!!["mimetype"]!!
|
||||
}
|
||||
|
||||
imageLoader.loadImage(holder.itemView.previewImage, previewMessageUrl, mutableMap)
|
||||
}
|
||||
} ?: run {
|
||||
holder.itemView.quotedPreviewImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
imageLoader.loadImage(holder.itemView.quotedUserAvatar, parentMessage.user.avatar)
|
||||
holder.itemView.quotedAuthor.text = parentMessage.actorDisplayName
|
||||
?: context.getText(R.string.nc_nick_guest)
|
||||
holder.itemView.quotedChatText.text = parentMessage.text
|
||||
holder.itemView.messageTime?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
} ?: run {
|
||||
holder.itemView.quotedMessageLayout.isVisible = false
|
||||
}
|
||||
|
||||
it.imageUrl?.let { imageUrl ->
|
||||
holder.itemView.previewImage.setOnClickListener {
|
||||
onElementClickPass?.invoke(page, holder, element, emptyMap())
|
||||
true
|
||||
}
|
||||
|
||||
if (imageUrl == "no-preview") {
|
||||
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
|
||||
holder.itemView.previewImage.visibility = View.VISIBLE
|
||||
imageLoader.getImageLoader().loadAny(context, getDrawableResourceIdForMimeType(it.selectedIndividualHashMap!!["mimetype"])) {
|
||||
target(holder.itemView.previewImage)
|
||||
}
|
||||
} else {
|
||||
holder.itemView.previewImage.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
holder.itemView.previewImage.visibility = View.VISIBLE
|
||||
val mutableMap = mutableMapOf<String, String>()
|
||||
if (it.selectedIndividualHashMap?.containsKey("mimetype") == true) {
|
||||
mutableMap["mimetype"] = it.selectedIndividualHashMap!!["mimetype"]!!
|
||||
}
|
||||
|
||||
imageLoader.loadImage(holder.itemView.previewImage, imageUrl, mutableMap)
|
||||
}
|
||||
} ?: run {
|
||||
holder.itemView.previewImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
} else {
|
||||
holder.itemView.systemMessageText.text = it.text
|
||||
holder.itemView.systemItemTime.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
|
||||
}
|
||||
}
|
||||
element.type == ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal -> {
|
||||
holder.itemView.noticeText.text = context.resources.getString(R.string.nc_new_messages)
|
||||
}
|
||||
else -> {
|
||||
// Date header
|
||||
holder.itemView.noticeText.text = (element.data as HeaderSource.Data<*, *>).header.toString()
|
||||
}
|
||||
}
|
||||
element.type == ChatElementTypes.UNREAD_MESSAGE_NOTICE.ordinal -> {
|
||||
holder.itemView.noticeText.text = context.resources.getString(R.string.nc_new_messages)
|
||||
}
|
||||
else -> {
|
||||
// Date header
|
||||
holder.itemView.noticeText.text = (element.data as HeaderSource.Data<*, *>).header.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import android.view.*
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import coil.api.load
|
||||
import coil.target.Target
|
||||
@ -71,8 +72,6 @@ import com.otaliastudios.elements.Adapter
|
||||
import com.otaliastudios.elements.Element
|
||||
import com.otaliastudios.elements.Page
|
||||
import com.otaliastudios.elements.Presenter
|
||||
import com.otaliastudios.elements.pagers.PageSizePager
|
||||
import com.stfalcon.chatkit.messages.MessagesListAdapter
|
||||
import com.uber.autodispose.lifecycle.LifecycleScopeProvider
|
||||
import kotlinx.android.synthetic.main.controller_chat.view.*
|
||||
import kotlinx.android.synthetic.main.lobby_view.view.*
|
||||
@ -93,7 +92,6 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
var conversationVoiceCallMenuItem: MenuItem? = null
|
||||
var conversationVideoMenuItem: MenuItem? = null
|
||||
|
||||
private lateinit var recyclerViewAdapter: MessagesListAdapter<ChatMessage>
|
||||
private lateinit var mentionAutocomplete: Autocomplete<*>
|
||||
|
||||
private var shouldShowLobby: Boolean = false
|
||||
@ -113,14 +111,30 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
viewModel.init(bundle.getParcelable(BundleKeys.KEY_USER)!!, bundle.getString(BundleKeys.KEY_CONVERSATION_TOKEN)!!, bundle.getString(KEY_CONVERSATION_PASSWORD))
|
||||
|
||||
messagesAdapter = Adapter.builder(this)
|
||||
.setPager(PageSizePager(80))
|
||||
//.addSource(ChatViewSource(itemsPerPage = 10))
|
||||
.addSource(ChatViewLiveDataSource(viewModel.messagesLiveData))
|
||||
.addSource(ChatDateHeaderSource(activity as Context, ChatElementTypes.DATE_HEADER.ordinal))
|
||||
.addPresenter(Presenter.forLoadingIndicator(activity as Context, R.layout.loading_state))
|
||||
.addPresenter(ChatPresenter(activity as Context, ::onElementClick, ::onElementLongClick, this))
|
||||
.setAutoScrollMode(Adapter.AUTOSCROLL_POSITION_0, true)
|
||||
.into(view.messagesRecyclerView)
|
||||
|
||||
messagesAdapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
val layoutManager = view.messagesRecyclerView.layoutManager as LinearLayoutManager
|
||||
if (layoutManager.findLastVisibleItemPosition() == positionStart - 1) {
|
||||
view.messagesRecyclerView.post {
|
||||
view.messagesRecyclerView.smoothScrollToPosition(positionStart + 1)
|
||||
}
|
||||
} else {
|
||||
// show popup
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
|
||||
layoutManager.stackFromEnd = true
|
||||
view.messagesRecyclerView.initRecyclerView(layoutManager, messagesAdapter, true)
|
||||
|
||||
viewModel.apply {
|
||||
conversation.observe(this@ChatView) { conversation ->
|
||||
setTitle()
|
||||
@ -151,7 +165,7 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
view.lobbyTextView?.setText(R.string.nc_lobby_waiting)
|
||||
}
|
||||
} else {
|
||||
view.messagesRecyclerView?.visibility = View.GONE
|
||||
view.messagesRecyclerView?.visibility = View.VISIBLE
|
||||
view.lobbyView?.visibility = View.GONE
|
||||
|
||||
if (isReadOnlyConversation) {
|
||||
@ -165,8 +179,8 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
return view
|
||||
}
|
||||
|
||||
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>) {
|
||||
if (element.type == ChatElementTypes.INCOMING_PREVIEW_MESSAGE.ordinal || element.type == ChatElementTypes.OUTGOING_PREVIEW_MESSAGE.ordinal) {
|
||||
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>, payload: Map<String, String>) {
|
||||
if (element.type == ChatElementTypes.CHAT_MESSAGE.ordinal) {
|
||||
element.data?.let { chatElement ->
|
||||
val chatMessage = chatElement.data as ChatMessage
|
||||
val currentUser = viewModel.user
|
||||
@ -228,21 +242,27 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>) {
|
||||
private fun onElementLongClick(page: Page, holder: Presenter.Holder, element: Element<ChatElement>, payload: Map<String, String>) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
viewModel.view = this
|
||||
setupViews()
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
viewModel.view = null
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater
|
||||
) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.menu_conversation_plus_filter, menu)
|
||||
inflater.inflate(R.menu.menu_conversation, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
@ -258,14 +278,14 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
conversationVoiceCallMenuItem?.isVisible = true
|
||||
conversationVideoMenuItem?.isVisible = true
|
||||
}
|
||||
|
||||
if (Conversation.ConversationType.ONE_TO_ONE_CONVERSATION == viewModel.conversation.value?.type) {
|
||||
loadAvatar()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
view?.let { view ->
|
||||
view.messagesRecyclerView.initRecyclerView(
|
||||
LinearLayoutManager(view.context), recyclerViewAdapter, false
|
||||
)
|
||||
|
||||
view.popupBubbleView.setRecyclerView(view.messagesRecyclerView)
|
||||
|
||||
val filters = arrayOfNulls<InputFilter>(1)
|
||||
@ -416,30 +436,32 @@ class ChatView(private val bundle: Bundle) : BaseView(), ImageLoaderInterface {
|
||||
|
||||
private fun loadAvatar() {
|
||||
val imageLoader = networkComponents.getImageLoader(viewModel.user)
|
||||
val avatarSize = DisplayUtils.convertDpToPixel(
|
||||
conversationVoiceCallMenuItem?.icon!!
|
||||
.intrinsicWidth.toFloat(), activity!!
|
||||
)
|
||||
.toInt()
|
||||
conversationVoiceCallMenuItem?.let {
|
||||
val avatarSize = DisplayUtils.convertDpToPixel(
|
||||
it.icon!!.intrinsicWidth.toFloat(), activity!!
|
||||
)
|
||||
.toInt()
|
||||
|
||||
avatarSize.let {
|
||||
val target = object : Target {
|
||||
override fun onSuccess(result: Drawable) {
|
||||
super.onSuccess(result)
|
||||
actionBar?.setIcon(result)
|
||||
avatarSize.let {
|
||||
val target = object : Target {
|
||||
override fun onSuccess(result: Drawable) {
|
||||
super.onSuccess(result)
|
||||
actionBar?.setIcon(result)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.conversation.value?.let {
|
||||
val avatarRequest = Images().getRequestForUrl(
|
||||
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
|
||||
viewModel.user.baseUrl,
|
||||
it.name, avatarSize / 2
|
||||
), viewModel.user, target, this,
|
||||
CircleCropTransformation()
|
||||
)
|
||||
imageLoader.load(avatarRequest)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.conversation.value?.let {
|
||||
val avatarRequest = Images().getRequestForUrl(
|
||||
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
|
||||
viewModel.user.baseUrl,
|
||||
it.name, avatarSize / 2
|
||||
), viewModel.user, target, this,
|
||||
CircleCropTransformation()
|
||||
)
|
||||
imageLoader.load(avatarRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
import android.app.Application
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||
import com.nextcloud.talk.models.json.chat.ChatOverall
|
||||
import com.nextcloud.talk.models.json.conversations.Conversation
|
||||
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
|
||||
import com.nextcloud.talk.newarch.data.model.ErrorModel
|
||||
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
|
||||
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
|
||||
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
|
||||
import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase
|
||||
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase
|
||||
import com.nextcloud.talk.newarch.domain.usecases.GetChatMessagesUseCase
|
||||
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
|
||||
import com.nextcloud.talk.newarch.local.models.User
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import com.nextcloud.talk.newarch.local.models.toUser
|
||||
import com.nextcloud.talk.newarch.local.models.toUserEntity
|
||||
import com.nextcloud.talk.newarch.services.GlobalService
|
||||
import com.nextcloud.talk.newarch.services.GlobalServiceInterface
|
||||
import com.nextcloud.talk.newarch.utils.NetworkComponents
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import retrofit2.Response
|
||||
|
||||
class ChatViewModel constructor(application: Application,
|
||||
private val joinConversationUseCase: JoinConversationUseCase,
|
||||
private val exitConversationUseCase: ExitConversationUseCase,
|
||||
private val networkComponents: NetworkComponents,
|
||||
private val apiErrorHandler: ApiErrorHandler,
|
||||
private val conversationsRepository: ConversationsRepository,
|
||||
private val messagesRepository: MessagesRepository,
|
||||
private val globalService: GlobalService) : BaseViewModel<ChatView>(application), GlobalServiceInterface {
|
||||
lateinit var user: User
|
||||
val conversation: MutableLiveData<Conversation?> = MutableLiveData()
|
||||
var initConversation: Conversation? = null
|
||||
val messagesLiveData = Transformations.switchMap(conversation) {
|
||||
it?.let {
|
||||
messagesRepository.getMessagesWithUserForConversation(it.conversationId!!)
|
||||
var pastStartingPoint: Long = -1
|
||||
val futureStartingPoint: MutableLiveData<Long> = MutableLiveData()
|
||||
private var initConversation: Conversation? = null
|
||||
|
||||
val messagesLiveData = Transformations.switchMap(futureStartingPoint) {futureStartingPoint ->
|
||||
conversation.value?.let {
|
||||
messagesRepository.getMessagesWithUserForConversationSince(it.databaseId!!, futureStartingPoint).map { chatMessagesList ->
|
||||
chatMessagesList.map { chatMessage ->
|
||||
chatMessage.activeUser = user.toUserEntity()
|
||||
chatMessage.parentMessage?.activeUser = chatMessage.activeUser
|
||||
if (chatMessage.systemMessageType != null && chatMessage.systemMessageType != ChatMessage.SystemMessageType.DUMMY) {
|
||||
ChatElement(chatMessage, ChatElementTypes.SYSTEM_MESSAGE)
|
||||
} else {
|
||||
ChatElement(chatMessage, ChatElementTypes.CHAT_MESSAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var conversationPassword: String? = null
|
||||
var view: Controller? = null
|
||||
|
||||
|
||||
fun init(user: User, conversationToken: String, conversationPassword: String?) {
|
||||
@ -71,7 +99,7 @@ class ChatViewModel constructor(application: Application,
|
||||
override suspend fun gotConversationInfoForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) {
|
||||
if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) {
|
||||
if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) {
|
||||
this.conversation.value = conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!)
|
||||
this.conversation.postValue(conversationsRepository.getConversationForUserWithToken(user.id!!, conversation.token!!))
|
||||
conversation.token?.let { conversationToken ->
|
||||
globalService.joinConversation(conversationToken, conversationPassword, this)
|
||||
}
|
||||
@ -80,7 +108,79 @@ class ChatViewModel constructor(application: Application,
|
||||
}
|
||||
|
||||
override suspend fun joinedConversationForUser(userNgEntity: UserNgEntity, conversation: Conversation?, operationStatus: GlobalServiceInterface.OperationStatus) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
if (operationStatus == GlobalServiceInterface.OperationStatus.STATUS_OK) {
|
||||
if (userNgEntity.id == user.id && conversation!!.token == initConversation?.token) {
|
||||
pullPastMessagesForUserAndConversation(userNgEntity, conversation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pullPastMessagesForUserAndConversation(userNgEntity: UserNgEntity, conversation: Conversation) {
|
||||
if (userNgEntity.id == user.id && conversation.token == initConversation?.token && view != null) {
|
||||
val getChatMessagesUseCase = GetChatMessagesUseCase(networkComponents.getRepository(true, userNgEntity.toUser()), apiErrorHandler)
|
||||
val lastReadMessageId = conversation.lastReadMessageId
|
||||
getChatMessagesUseCase.invoke(viewModelScope, parametersOf(user, conversation.token, 0, lastReadMessageId, 1), object : UseCaseResponse<Response<ChatOverall>> {
|
||||
override suspend fun onSuccess(result: Response<ChatOverall>) {
|
||||
val messages = result.body()?.ocs?.data
|
||||
messages?.let {
|
||||
for (message in it) {
|
||||
message.activeUser = userNgEntity
|
||||
message.internalConversationId = conversation.databaseId
|
||||
}
|
||||
|
||||
messagesRepository.saveMessagesForConversation(it)
|
||||
}
|
||||
|
||||
val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given")
|
||||
if (xChatLastGivenHeader != null) {
|
||||
pastStartingPoint = xChatLastGivenHeader.toLong()
|
||||
}
|
||||
|
||||
futureStartingPoint.postValue(pastStartingPoint)
|
||||
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, pastStartingPoint.toInt())
|
||||
}
|
||||
|
||||
override suspend fun onError(errorModel: ErrorModel?) {
|
||||
// What to do here
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pullFutureMessagesForUserAndConversation(userNgEntity: UserNgEntity, conversation: Conversation, lastGivenMessage: Int = 0) {
|
||||
if (userNgEntity.id == user.id && conversation.token == initConversation?.token && view != null) {
|
||||
val getChatMessagesUseCase = GetChatMessagesUseCase(networkComponents.getRepository(true, userNgEntity.toUser()), apiErrorHandler)
|
||||
var lastKnownMessageId = lastGivenMessage
|
||||
if (lastGivenMessage == 0) {
|
||||
lastKnownMessageId = conversation.lastReadMessageId.toInt()
|
||||
}
|
||||
getChatMessagesUseCase.invoke(viewModelScope, parametersOf(user, conversation.token, 1, lastKnownMessageId, 0), object : UseCaseResponse<Response<ChatOverall>> {
|
||||
override suspend fun onSuccess(result: Response<ChatOverall>) {
|
||||
val messages = result.body()?.ocs?.data
|
||||
messages?.let {
|
||||
for (message in it) {
|
||||
message.activeUser = userNgEntity
|
||||
message.internalConversationId = conversation.databaseId
|
||||
}
|
||||
|
||||
messagesRepository.saveMessagesForConversation(it)
|
||||
}
|
||||
|
||||
if (result.code() == 200) {
|
||||
val xChatLastGivenHeader: String? = result.headers().get("X-Chat-Last-Given")
|
||||
if (xChatLastGivenHeader != null) {
|
||||
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, xChatLastGivenHeader.toInt())
|
||||
}
|
||||
} else {
|
||||
pullFutureMessagesForUserAndConversation(userNgEntity, conversation, lastKnownMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onError(errorModel: ErrorModel?) {
|
||||
pullFutureMessagesForUserAndConversation(userNgEntity, conversation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -25,16 +25,18 @@ package com.nextcloud.talk.newarch.features.chat
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
|
||||
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
|
||||
import com.nextcloud.talk.newarch.domain.repository.offline.MessagesRepository
|
||||
import com.nextcloud.talk.newarch.domain.usecases.ExitConversationUseCase
|
||||
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase
|
||||
import com.nextcloud.talk.newarch.services.GlobalService
|
||||
import com.nextcloud.talk.newarch.utils.NetworkComponents
|
||||
|
||||
class ChatViewModelFactory constructor(
|
||||
private val application: Application,
|
||||
private val joinConversationUseCase: JoinConversationUseCase,
|
||||
private val exitConversationUseCase: ExitConversationUseCase,
|
||||
private val networkComponents: NetworkComponents,
|
||||
private val apiErrorHandler: ApiErrorHandler,
|
||||
private val conversationsRepository: ConversationsRepository,
|
||||
private val messagesRepository: MessagesRepository,
|
||||
private val globalService: GlobalService
|
||||
@ -42,7 +44,7 @@ class ChatViewModelFactory constructor(
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return ChatViewModel(
|
||||
application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository, globalService
|
||||
application, networkComponents, apiErrorHandler, conversationsRepository, messagesRepository, globalService
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
@ -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.groupconversation.GroupConversationView
|
||||
import com.nextcloud.talk.newarch.features.search.DebouncingTextWatcher
|
||||
import com.nextcloud.talk.newarch.local.models.toUser
|
||||
import com.nextcloud.talk.newarch.mvvm.BaseView
|
||||
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
|
||||
import com.nextcloud.talk.newarch.utils.ElementPayload
|
||||
@ -179,10 +180,13 @@ class ContactsView(private val bundle: Bundle? = null) : BaseView() {
|
||||
ContactsViewOperationState.OK -> {
|
||||
val bundle = Bundle()
|
||||
if (!hasToken || isNewGroupConversation) {
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
|
||||
router.replaceTopController(RouterTransaction.with(ChatView(bundle))
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
.pushChangeHandler(HorizontalChangeHandler()))
|
||||
globalService.currentUserLiveData.value?.let {
|
||||
bundle.putParcelable(BundleKeys.KEY_USER, it.toUser())
|
||||
bundle.putString(BundleKeys.KEY_CONVERSATION_TOKEN, operationState.conversationToken)
|
||||
router.replaceTopController(RouterTransaction.with(ChatView(bundle))
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
.pushChangeHandler(HorizontalChangeHandler()))
|
||||
}
|
||||
} else {
|
||||
// we added the participants - go back to conversations info
|
||||
router.popCurrentController()
|
||||
|
@ -155,6 +155,7 @@ open class ConversationPresenter(context: Context, onElementClick: ((Page, Holde
|
||||
addHeader("Authorization", user.getCredentials())
|
||||
transformations(CircleCropTransformation())
|
||||
fallback(Images().getImageForConversation(context, conversation, true))
|
||||
error(Images().getImageForConversation(context, conversation, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
package com.nextcloud.talk.newarch.features.conversationsList
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@ -82,7 +83,7 @@ class ConversationsListView : BaseView() {
|
||||
|
||||
val adapter = Adapter.builder(this)
|
||||
.addSource(ConversationsListSource(viewModel.conversationsLiveData))
|
||||
.addPresenter(ConversationPresenter(context, ::onElementClick, ::onElementLongClick))
|
||||
.addPresenter(ConversationPresenter(activity as Context, ::onElementClick, ::onElementLongClick))
|
||||
.addPresenter(Presenter.forLoadingIndicator(context, R.layout.loading_state))
|
||||
.addPresenter(AdvancedEmptyPresenter(context, R.layout.message_state, ::openNewConversationScreen) { view ->
|
||||
view.messageStateImageView.imageTintList = resources?.getColor(R.color.colorPrimary)?.let { ColorStateList.valueOf(it) }
|
||||
@ -163,7 +164,7 @@ class ConversationsListView : BaseView() {
|
||||
conversation?.let { conversation ->
|
||||
val bundle = Bundle()
|
||||
with(bundle) {
|
||||
putParcelable(BundleKeys.KEY_USER_ENTITY, user)
|
||||
putParcelable(BundleKeys.KEY_USER, user.toUser())
|
||||
putString(BundleKeys.KEY_CONVERSATION_TOKEN, conversation.token)
|
||||
putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId)
|
||||
putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))
|
||||
|
@ -179,11 +179,13 @@ class ConversationsListViewModel (
|
||||
operationUser?.let {
|
||||
viewModelScope.launch {
|
||||
val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 256)
|
||||
val drawable = Coil.get((url)) {
|
||||
addHeader("Authorization", it.getCredentials())
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
avatar.postValue(drawable)
|
||||
try {
|
||||
val drawable = Coil.get((url)) {
|
||||
addHeader("Authorization", it.getCredentials())
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
avatar.postValue(drawable)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
@Query("SELECT * FROM conversations where id = :internalUserId AND token = :token")
|
||||
abstract suspend fun getConversationForUserWithToken(internalUserId: Long, token: String): ConversationEntity?
|
||||
@Query("SELECT * FROM conversations where user_id = :userId AND token = :token")
|
||||
abstract suspend fun getConversationForUserWithToken(userId: Long, token: String): ConversationEntity?
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateConversationsForUser(
|
||||
|
@ -31,10 +31,13 @@ import com.nextcloud.talk.newarch.local.models.MessageEntity
|
||||
|
||||
@Dao
|
||||
abstract class MessagesDao {
|
||||
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId")
|
||||
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId ORDER BY message_id ASC")
|
||||
abstract fun getMessagesWithUserForConversation(conversationId: String):
|
||||
LiveData<List<MessageEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun saveMessagesWithInsert(vararg messages: MessageEntity): List<Long>
|
||||
abstract suspend fun saveMessages(vararg messages: MessageEntity): List<Long>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE conversation_id = :conversationId AND message_id >= :messageId ORDER BY message_id ASC")
|
||||
abstract fun getMessagesWithUserForConversationSince(conversationId: String, messageId: Long): LiveData<List<MessageEntity>>
|
||||
}
|
@ -34,6 +34,7 @@ import com.nextcloud.talk.newarch.local.dao.UsersDao
|
||||
import com.nextcloud.talk.newarch.local.models.ConversationEntity
|
||||
import com.nextcloud.talk.newarch.local.models.MessageEntity
|
||||
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
import org.parceler.converter.HashMapParcelConverter
|
||||
|
||||
@Database(
|
||||
entities = [ConversationEntity::class, MessageEntity::class, UserNgEntity::class],
|
||||
@ -46,7 +47,8 @@ import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
||||
ConversationTypeConverter::class, ParticipantTypeConverter::class,
|
||||
PushConfigurationConverter::class, CapabilitiesConverter::class,
|
||||
SignalingSettingsConverter::class,
|
||||
UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class
|
||||
UserStatusConverter::class, SystemMessageTypeConverter::class, ParticipantMapConverter::class,
|
||||
HashMapHashMapConverter::class
|
||||
)
|
||||
|
||||
abstract class TalkDatabase : RoomDatabase() {
|
||||
|
@ -48,13 +48,12 @@ data class MessageEntity(
|
||||
@ColumnInfo(name = "actor_display_name") var actorDisplayName: String? = null,
|
||||
@ColumnInfo(name = "timestamp") var timestamp: Long = 0,
|
||||
@ColumnInfo(name = "message") var message: String? = null,
|
||||
/*@JsonField(name = "messageParameters")
|
||||
public HashMap<String, HashMap<String, String>> messageParameters;*/
|
||||
@ColumnInfo(name = "messageParameters") var messageParameters: HashMap<String, HashMap<String, String>>? = null,
|
||||
@ColumnInfo(name = "parent") var parentMessage: ChatMessage? = null,
|
||||
@ColumnInfo(name = "replyable") var replyable: Boolean = false,
|
||||
@ColumnInfo(name = "system_message_type") var systemMessageType: SystemMessageType? = null
|
||||
)
|
||||
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
fun MessageEntity.toChatMessage(): ChatMessage {
|
||||
val chatMessage = ChatMessage()
|
||||
chatMessage.internalMessageId = this.id
|
||||
@ -65,15 +64,15 @@ fun MessageEntity.toChatMessage(): ChatMessage {
|
||||
chatMessage.actorDisplayName = this.actorDisplayName
|
||||
chatMessage.timestamp = this.timestamp
|
||||
chatMessage.message = this.message
|
||||
//chatMessage.messageParameters = this.messageParameters
|
||||
chatMessage.messageParameters = this.messageParameters
|
||||
chatMessage.systemMessageType = this.systemMessageType
|
||||
chatMessage.replyable = this.replyable
|
||||
chatMessage.parentMessage = this.parentMessage
|
||||
return chatMessage
|
||||
}
|
||||
|
||||
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
|
||||
fun ChatMessage.toMessageEntity(): MessageEntity {
|
||||
val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.activeUser!!.id.toString() + "@" + this.internalConversationId)
|
||||
val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.internalConversationId!!)
|
||||
messageEntity.messageId = this.jsonMessageId!!
|
||||
messageEntity.actorType = this.actorType
|
||||
messageEntity.actorId = this.actorId
|
||||
@ -82,7 +81,8 @@ fun ChatMessage.toMessageEntity(): MessageEntity {
|
||||
messageEntity.message = this.message
|
||||
messageEntity.systemMessageType = this.systemMessageType
|
||||
messageEntity.replyable = this.replyable
|
||||
//messageEntity.messageParameters = this.messageParameters
|
||||
messageEntity.messageParameters = this.messageParameters
|
||||
messageEntity.parentMessage = this.parentMessage
|
||||
|
||||
return messageEntity
|
||||
}
|
||||
|
@ -55,7 +55,6 @@ class GlobalService constructor(usersRepository: UsersRepository,
|
||||
user?.let {
|
||||
if (it.id != previousUser?.id) {
|
||||
cookieManager.cookieStore.removeAll()
|
||||
//okHttpClient.dispatcher().cancelAll()
|
||||
currentConversation = null
|
||||
}
|
||||
}
|
||||
|
@ -129,10 +129,16 @@ class ShortcutService constructor(private var context: Context,
|
||||
iconImage = images.getImageForConversation(context, conversation)
|
||||
|
||||
if (iconImage == null) {
|
||||
iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) {
|
||||
addHeader("Authorization", user.getCredentials())
|
||||
transformations(CircleCropTransformation())
|
||||
try {
|
||||
iconImage = Coil.get(ApiUtils.getUrlForAvatarWithName(user.baseUrl, conversation.name, R.dimen.avatar_size_big)) {
|
||||
addHeader("Authorization", user.getCredentials())
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// no icon, that's fine for now
|
||||
iconImage = images.getImageForConversation(context, conversation, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shortcuts.add(ShortcutInfoCompat.Builder(context, "current_conversation_" + (index + 1))
|
||||
|
@ -47,6 +47,7 @@ class NetworkComponents(
|
||||
val usersMultipleOperationsRepositoryMap: MutableMap<Long, NextcloudTalkRepository> = mutableMapOf()
|
||||
val usersSingleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
|
||||
val usersMultipleOperationOkHttpMap: MutableMap<Long, OkHttpClient> = mutableMapOf()
|
||||
val usersImageLoaderMap: MutableMap<Long, ImageLoader> = mutableMapOf()
|
||||
|
||||
fun getRepository(singleOperation: Boolean, user: User): NextcloudTalkRepository {
|
||||
val mappedNextcloudTalkRepository = if (singleOperation) {
|
||||
@ -89,20 +90,28 @@ class NetworkComponents(
|
||||
}
|
||||
|
||||
fun getImageLoader(user: User): ImageLoader {
|
||||
return ImageLoader(androidApplication) {
|
||||
availableMemoryPercentage(0.5)
|
||||
bitmapPoolPercentage(0.5)
|
||||
crossfade(false)
|
||||
okHttpClient(getOkHttpClient(false, user))
|
||||
componentRegistry {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder())
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
var mappedImageLoader = usersImageLoaderMap[user.id]
|
||||
|
||||
if (mappedImageLoader == null) {
|
||||
mappedImageLoader = ImageLoader(androidApplication) {
|
||||
availableMemoryPercentage(0.5)
|
||||
bitmapPoolPercentage(0.5)
|
||||
crossfade(false)
|
||||
okHttpClient(getOkHttpClient(false, user))
|
||||
componentRegistry {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
add(ImageDecoderDecoder())
|
||||
} else {
|
||||
add(GifDecoder())
|
||||
}
|
||||
add(SvgDecoder(androidApplication))
|
||||
}
|
||||
add(SvgDecoder(androidApplication))
|
||||
}
|
||||
|
||||
usersImageLoaderMap[user.id!!] = mappedImageLoader
|
||||
}
|
||||
|
||||
return mappedImageLoader
|
||||
|
||||
}
|
||||
}
|
@ -51,15 +51,6 @@ public class ArbitraryStorageUtils {
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public ArbitraryStorageEntity getStorageSetting(long accountIdentifier, String key,
|
||||
@Nullable String object) {
|
||||
Result findStorageQueryResult = dataStore.select(ArbitraryStorage.class)
|
||||
.where(ArbitraryStorageEntity.ACCOUNT_IDENTIFIER.eq(accountIdentifier)
|
||||
.and(ArbitraryStorageEntity.KEY.eq(key)).and(ArbitraryStorageEntity.OBJECT.eq(object)))
|
||||
.limit(1).get();
|
||||
|
||||
return (ArbitraryStorageEntity) findStorageQueryResult.firstOrNull();
|
||||
}
|
||||
|
||||
public Observable deleteAllEntriesForAccountIdentifier(long accountIdentifier) {
|
||||
ReactiveScalar<Integer> deleteResult = dataStore.delete(ArbitraryStorage.class)
|
||||
|
@ -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_height="match_parent"
|
||||
android:layout_above="@+id/separator"
|
||||
app:stackFromEnd="true"
|
||||
app:reverseLayout="true"
|
||||
android:id="@+id/messagesRecyclerView"/>
|
||||
|
||||
<View
|
||||
|
@ -1,77 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/quotedChatMessageView"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/quotedMessageLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<View
|
||||
android:id="@+id/quoteColoredView"
|
||||
android:layout_width="2dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@id/flexboxQuoted"
|
||||
android:layout_height="100dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignBottom="@id/quotedChatText"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@color/colorPrimary"/>
|
||||
android:background="@color/colorPrimary"
|
||||
/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/quoteColoredView"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:id="@+id/quotedAuthorLayout">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/quotedUserAvatar"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:shapeAppearanceOverlay="@style/circleImageView"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/quotedUserAvatar"
|
||||
android:textSize="12sp"
|
||||
android:id="@+id/quotedAuthor"
|
||||
android:layout_alignBaseline="@id/quotedUserAvatar"
|
||||
tools:text="Another user"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quotedPreviewImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/quotedAuthorLayout"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_toStartOf="@id/cancelReplyButton"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:src="@tools:sample/backgrounds/scenic"/>
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/quotedMessageAuthor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:ellipsize="end"
|
||||
android:textSize="12sp"
|
||||
tools:text="Mario" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/flexboxQuoted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/quotedMessageAuthor"
|
||||
android:layout_alignStart="@id/quotedMessageAuthor"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_toStartOf="@id/cancelReplyButton"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="vertical"
|
||||
app:alignContent="stretch"
|
||||
app:alignItems="stretch"
|
||||
app:flexWrap="wrap"
|
||||
app:justifyContent="flex_start">
|
||||
android:id="@+id/quotedChatText"
|
||||
android:layout_below="@id/quotedPreviewImage"
|
||||
tools:text="Just another chat message"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quotedMessageImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_alignSelf="flex_start"
|
||||
app:layout_flexGrow="1"
|
||||
app:layout_wrapBefore="true"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/quotedChatText"
|
||||
android:textSize="12sp"
|
||||
android:id="@+id/quotedMessageTime"
|
||||
android:layout_marginEnd="8dp"
|
||||
tools:text="12:30"/>
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/quotedMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/quotedMessageImage"
|
||||
android:layout_alignStart="@id/quotedMessageAuthor"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textSize="14sp"
|
||||
app:layout_alignSelf="flex_start"
|
||||
app:layout_flexGrow="1"
|
||||
app:layout_wrapBefore="true"
|
||||
tools:text="Hello, this is me!" />
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="@drawable/ic_cancel_black_24dp"
|
||||
|
@ -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"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent" android:layout_height="match_parent">
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noticeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAlignment="center"
|
||||
android:padding="16dp"/>
|
||||
|
||||
</RelativeLayout>
|
@ -36,7 +36,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0-alpha03'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0-alpha04'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
|
||||
|
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
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-rc-1-bin.zip
|
||||
|
Loading…
Reference in New Issue
Block a user