Fix some of the issues

This commit is contained in:
Mario Danic 2019-12-18 03:34:07 +01:00
parent a9933b484c
commit 4cd1b9fb18
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
30 changed files with 625 additions and 151 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "26585a95894baebb7d811a80f3811b85",
"identityHash": "0bb77d35d80f74e97eea4a5415d9a102",
"entities": [
{
"tableName": "conversations",
@ -15,7 +15,7 @@
"notNull": true
},
{
"fieldPath": "user",
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "INTEGER",
"notNull": false
@ -198,7 +198,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, `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, `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",
@ -207,7 +207,7 @@
"notNull": true
},
{
"fieldPath": "conversation",
"fieldPath": "conversationId",
"columnName": "conversation_id",
"affinity": "TEXT",
"notNull": true
@ -248,6 +248,12 @@
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "replyable",
"columnName": "replyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "system_message_type",
@ -369,7 +375,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, '26585a95894baebb7d811a80f3811b85')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0bb77d35d80f74e97eea4a5415d9a102')"
]
}
}

View File

@ -34,6 +34,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType.ONE_TO_ONE_CONVERSATION
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.ApiUtils
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -67,14 +68,14 @@ class ConversationItem(
&& model.unreadMention == comparedConversation.unreadMention
&& model.objectType == comparedConversation.objectType
&& model.changing == comparedConversation.changing
&& user.id == inItem.user.id)
&& inItem.user.id == other.user.id)
}
return false
}
override fun hashCode(): Int {
return Objects.hash(
model.conversationId, model.token,
model.token,
user.id
)
}
@ -245,6 +246,7 @@ class ConversationItem(
model.name, R.dimen.avatar_size
)
) {
addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation())
}

View File

@ -27,6 +27,7 @@ import coil.transform.CircleCropTransformation
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.utils.ApiUtils
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -112,6 +113,7 @@ class MentionAutocompleteItem(
}
holder.avatarImageView!!.load(avatarUrl) {
addHeader("Authorization", currentUser.getCredentials())
transformations(CircleCropTransformation())
}
}

View File

@ -59,6 +59,7 @@ import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.newarch.utils.getCredentials
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DoNotDisturbUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
@ -393,6 +394,7 @@ class CallNotificationController(private val originalBundle: Bundle) : BaseContr
currentConversation!!.name, R.dimen.avatar_size_very_big
)
) {
addHeader("Authorization", userBeingCalled.getCredentials())
transformations(CircleCropTransformation())
listener(onSuccess = { data, dataSource ->
GlobalScope.launch {

View File

@ -105,7 +105,6 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
.OnMessageLongClickListener<IMessage>, MessageHolders.ContentChecker<IMessage> {
val ncApi: NcApi by inject()
val userUtils: UserUtils by inject()
@BindView(R.id.messagesListView)
@JvmField
@ -306,7 +305,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
conversationUser?.baseUrl,
currentConversation?.name, avatarSize / 2
), null, target, null,
), conversationUser, target, null,
CircleCropTransformation()
)

View File

@ -256,10 +256,10 @@ class SettingsController : BaseController() {
}
private fun removeCurrentAccount() {
val user = usersRepository.getActiveUser()
user!!.status = UserStatus.PENDING_DELETE
GlobalScope.launch {
val job = async {
val user = usersRepository.getActiveUser()
user!!.status = UserStatus.PENDING_DELETE
usersRepository.updateUser(user)
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java)
.build()
@ -274,13 +274,14 @@ class SettingsController : BaseController() {
onAttach(view!!)
}
} else {
router.setRoot(RouterTransaction.with(
ServerSelectionController()
)
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
)
withContext(Dispatchers.Main) {
router.setRoot(RouterTransaction.with(
ServerSelectionController()
)
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
)
}
}
}
}
@ -293,7 +294,7 @@ class SettingsController : BaseController() {
}
GlobalScope.launch {
var hasMultipleUsers: Boolean = false
var hasMultipleUsers = false
val job = async {
currentUser = usersRepository.getActiveUser()
hasMultipleUsers = usersRepository.getUsers().size > 0

View File

@ -37,12 +37,21 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.AdvancedUserItem
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.models.ImportAccount
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.AccountUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.UserUtils
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import java.net.CookieManager
import java.util.*
@ -53,7 +62,7 @@ class SwitchAccountController : BaseController {
internal var recyclerView: RecyclerView? = null
val cookieManager: CookieManager by inject()
val userUtils: UserUtils by inject()
val usersRepository: UsersRepository by inject()
@JvmField
@BindView(R.id.swipe_refresh_layout)
@ -75,30 +84,13 @@ class SwitchAccountController : BaseController {
private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { view, position ->
if (userItems.size > position) {
val userEntity = (userItems[position] as AdvancedUserItem).entity
/*userUtils!!.createOrUpdateUser(null, null, null, null, null, true, null, userEntity!!.getId(), null, null, null)
.`as`(AutoDispose.autoDisposable<Any>(scopeProvider))
.subscribe(object : Observer<UserEntity> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(userEntity: UserEntity) {
cookieManager!!.cookieStore.removeAll()
userUtils!!.disableAllUsersWithoutId(userEntity.getId())
if (activity != null) {
activity!!.runOnUiThread { router.popCurrentController() }
}
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
}
})*/
GlobalScope.launch {
usersRepository.setUserAsActiveWithId(userEntity!!.id!!)
cookieManager.cookieStore.removeAll()
withContext(Dispatchers.Main) {
router.popCurrentController()
}
}
}
true
@ -130,7 +122,69 @@ class SwitchAccountController : BaseController {
return inflater.inflate(R.layout.controller_generic_rv, container, false)
}
override fun onViewBound(view: View) {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
swipeRefreshLayout?.isEnabled = false
actionBar?.show()
adapter = FlexibleAdapter(userItems, activity, false)
GlobalScope.launch {
val users = usersRepository.getUsers()
var userEntity: UserNgEntity
var participant: Participant
if (isAccountImport) {
var account: Account
var importAccount: ImportAccount
for (accountObject in AccountUtils.findAccounts(users)) {
account = accountObject
importAccount = AccountUtils.getInformationFromAccount(account)
participant = Participant()
participant.name = importAccount.username
participant.userId = importAccount.username
userEntity = UserNgEntity(-1, "!", "!", importAccount.baseUrl)
userItems.add(AdvancedUserItem(participant, userEntity, account))
}
adapter!!.addListener(onImportItemClickListener)
withContext(Dispatchers.Main) {
adapter!!.updateDataSet(userItems, false)
}
} else {
for (userEntityObject in users) {
userEntity = userEntityObject
if (userEntity.status != UserStatus.ACTIVE) {
participant = Participant()
participant.name = userEntity.displayName
val userId: String
if (userEntity.userId != null) {
userId = userEntity.userId
} else {
userId = userEntity.username
}
participant.userId = userId
userItems.add(AdvancedUserItem(participant, userEntity, null))
}
}
adapter!!.addListener(onSwitchItemClickListener)
withContext(Dispatchers.Main) {
adapter!!.updateDataSet(userItems, false)
}
}
}
return super.onCreateView(inflater, container)
}
override fun onViewBound(view: View) {
super.onViewBound(view)
swipeRefreshLayout!!.isEnabled = false

View File

@ -486,7 +486,7 @@ class NotificationWorker(
}
val request = Images().getRequestForUrl(
Coil.loader(), context!!, avatarUrl!!, null,
Coil.loader(), context!!, avatarUrl!!, signatureVerification!!.userEntity,
target, null, CircleCropTransformation()
)

View File

@ -44,6 +44,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Data;

View File

@ -29,6 +29,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.converters.*
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.newarch.local.models.ConversationEntity
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import lombok.Data
import org.parceler.Parcel
@ -42,8 +43,6 @@ class Conversation {
@JsonIgnore
@NonNull
var internalUserId: Long? = null
@JsonIgnore
var internalId: Long? = null
@JsonField(name = ["id"])
var conversationId: String? = null
@JsonField(name = ["token"])

View File

@ -20,9 +20,7 @@
package com.nextcloud.talk.newarch.data.repository.offline
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.lifecycle.*
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
import com.nextcloud.talk.newarch.local.dao.ConversationsDao
@ -58,14 +56,11 @@ class ConversationsRepositoryImpl(val conversationsDao: ConversationsDao) :
}
override fun getConversationsForUser(userId: Long): LiveData<List<Conversation>> {
return Transformations.distinctUntilChanged(conversationsDao
.getConversationsForUser(userId)
.map { data ->
data.map {
it.toConversation()
}
}
)
return conversationsDao.getConversationsForUser(userId).distinctUntilChanged().map { data ->
data.map {
it.toConversation()
}
}
}
override suspend fun getConversationForUserWithToken(userId: Long, token: String): Conversation? {

View File

@ -21,13 +21,15 @@
package com.nextcloud.talk.newarch.data.repository.offline
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.distinctUntilChanged
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.dao.UsersDao
import com.nextcloud.talk.newarch.local.models.UserNgEntity
class UsersRepositoryImpl(val usersDao: UsersDao) : UsersRepository {
class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
override fun getActiveUserLiveData(): LiveData<UserNgEntity> {
return usersDao.getActiveUserLiveData()
return usersDao.getActiveUserLiveData().distinctUntilChanged()
}
override fun getActiveUser(): UserNgEntity {

View File

@ -55,18 +55,26 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou
return apiService.getConversation(userEntity.getCredentials(), conversationToken)
}
override suspend fun joinConversationForUser(userNgEntity: UserNgEntity, conversationToken: String, conversationPassword: String?): RoomOverall {
return apiService.joinConversation(userNgEntity.getCredentials(), ApiUtils.getUrlForSettingMyselfAsActiveParticipant(userNgEntity.baseUrl, conversationToken), conversationPassword)
}
override suspend fun exitConversationForUser(userNgEntity: UserNgEntity, conversationToken: String): GenericOverall {
return apiService.exitConversation(userNgEntity.getCredentials(), ApiUtils.getUrlForSettingMyselfAsActiveParticipant(userNgEntity.baseUrl, conversationToken))
}
override suspend fun setFavoriteValueForConversation(
user: UserNgEntity,
conversation: Conversation,
favorite: Boolean
): GenericOverall {
if (favorite) {
return apiService.addConversationToFavorites(
return if (favorite) {
apiService.addConversationToFavorites(
user.getCredentials(),
ApiUtils.getUrlForConversationFavorites(user.baseUrl, conversation.token)
)
} else {
return apiService.removeConversationFromFavorites(
apiService.removeConversationFromFavorites(
user.getCredentials(),
ApiUtils.getUrlForConversationFavorites(user.baseUrl, conversation.token)
)

View File

@ -23,6 +23,7 @@ package com.nextcloud.talk.newarch.data.source.remote
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable
import retrofit2.http.*
interface ApiService {
@ -63,4 +64,14 @@ interface ApiService {
@GET
suspend fun getConversation(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
@FormUrlEncoded
@POST
suspend fun joinConversation(@Header("Authorization") authorization: String,
@Url url: String, @Field("password") password: String?): RoomOverall
@DELETE
suspend fun exitConversation(@Header("Authorization") authorization: String,
@Url url: String): GenericOverall
}

View File

@ -47,4 +47,15 @@ interface NextcloudTalkRepository {
userEntity: UserNgEntity,
conversationToken: String
): RoomOverall
suspend fun joinConversationForUser(
userNgEntity: UserNgEntity,
conversationToken: String,
conversationPassword: String?
): RoomOverall
suspend fun exitConversationForUser(
userNgEntity: UserNgEntity,
conversationToken: String
): GenericOverall
}

View File

@ -0,0 +1,40 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 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.generic.GenericOverall
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
class ExitConversationUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<GenericOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.exitConversationForUser(
definitionParameters[0],
definitionParameters[1])
}
}

View File

@ -0,0 +1,42 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 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.conversations.RoomOverall
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
class JoinConversationUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<RoomOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): RoomOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.joinConversationForUser(
definitionParameters[0],
definitionParameters[1],
definitionParameters[2]
)
}
}

View File

@ -355,7 +355,8 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
}
}
if (url.startsWith(viewModel.user.baseUrl) && url.contains("index.php/core/preview?fileId=")) {
val needsAuthBasedOnUrl = url.contains("index.php/core/preview?fileId=") || url.contains("index.php/avatar/")
if (url.startsWith(viewModel.user.baseUrl) && needsAuthBasedOnUrl) {
addHeader("Authorization", viewModel.user.getCredentials())
}
@ -384,7 +385,7 @@ class ChatView : BaseView(), MessageHolders.ContentChecker<IMessage>, MessagesLi
imageLoader, context, ApiUtils.getUrlForAvatarWithNameAndPixels(
viewModel.user.baseUrl,
it.name, avatarSize / 2
), null, target, this,
), viewModel.user, target, this,
CircleCropTransformation()
)

View File

@ -2,16 +2,37 @@ package com.nextcloud.talk.newarch.features.chat
import android.app.Application
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel
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.base.UseCaseResponse
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListViewState
import com.nextcloud.talk.newarch.local.models.MessageEntity
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class ChatViewModel constructor(application: Application, private val conversationsRepository: ConversationsRepository) : BaseViewModel<ChatView>(application) {
class ChatViewModel constructor(application: Application,
private val joinConversationUseCase: JoinConversationUseCase,
private val exitConversationUseCase: ExitConversationUseCase,
private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository) : BaseViewModel<ChatView>(application) {
lateinit var user: UserNgEntity
val conversation: MutableLiveData<Conversation?> = MutableLiveData()
val messagesLiveData = Transformations.switchMap(conversation) {
it?.let {
messagesRepository.getMessagesWithUserForConversation(it.conversationId!!)
}
}
var conversationPassword: String? = null
@ -23,6 +44,38 @@ class ChatViewModel constructor(application: Application, private val conversati
}
}
suspend fun joinConversation() {
joinConversationUseCase.invoke(viewModelScope, parametersOf(
user,
conversation.value!!.token,
conversationPassword
),
object : UseCaseResponse<RoomOverall> {
override suspend fun onSuccess(result: RoomOverall) {
conversationsRepository.saveConversationsForUser(user.id!!, listOf(result.ocs.data))
}
override fun onError(errorModel: ErrorModel?) {
// what do we do on error
}
})
}
suspend fun exitConversation() {
exitConversationUseCase.invoke(backgroundScope, parametersOf(
user,
conversation.value!!.token
),
object : UseCaseResponse<GenericOverall> {
override suspend fun onSuccess(result: GenericOverall) {
}
override fun onError(errorModel: ErrorModel?) {
// what do we do on error
}
})
}
fun sendMessage(message: CharSequence) {
}

View File

@ -4,15 +4,21 @@ import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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
class ChatViewModelFactory constructor(
private val application: Application,
private val conversationsRepository: ConversationsRepository
private val joinConversationUseCase: JoinConversationUseCase,
private val exitConversationUseCase: ExitConversationUseCase,
private val conversationsRepository: ConversationsRepository,
private val messagesRepository: MessagesRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ChatViewModel(
application, conversationsRepository
application, joinConversationUseCase, exitConversationUseCase, conversationsRepository, messagesRepository
) as T
}
}

View File

@ -26,12 +26,15 @@ import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.view.MenuItemCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.distinctUntilChanged
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import butterknife.OnClick
import coil.ImageLoader
@ -74,11 +77,12 @@ import java.util.*
class ConversationsListView : BaseView(), OnQueryTextListener,
OnItemClickListener, OnItemLongClickListener {
lateinit var viewModel: ConversationsListViewModel
private lateinit var viewModel: ConversationsListViewModel
val factory: ConversationListViewModelFactory by inject()
val imageLoader: ImageLoader by inject()
private val imageLoader: ImageLoader by inject()
private val viewState: MutableLiveData<ConversationsListViewState> = MutableLiveData(LOADING)
private val recyclerViewAdapter = FlexibleAdapter(mutableListOf(), null, true)
private val recyclerViewAdapter = FlexibleAdapter(mutableListOf(), null, false)
private var searchItem: MenuItem? = null
private var settingsItem: MenuItem? = null
@ -178,9 +182,12 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
searchView!!.imeOptions = imeOptions
searchView!!.queryHint = resources?.getString(R.string.nc_search)
searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
searchView!!.setOnQueryTextListener(this)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
viewModel.loadConversations()
}
override fun onQueryTextSubmit(query: String?): Boolean {
@ -208,71 +215,15 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
loadAvatar()
})
viewState.observe(this@ConversationsListView, Observer { value ->
when (value) {
LOADING -> {
view?.swipeRefreshLayoutView?.isEnabled = false
view?.loadingStateView?.visibility = View.VISIBLE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
view?.floatingActionButton?.visibility = View.GONE
searchItem?.isVisible = false
}
LOADED -> {
view?.swipeRefreshLayoutView?.isEnabled = true
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.loadingStateView?.visibility = View.GONE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.VISIBLE
view?.floatingActionButton?.visibility = View.VISIBLE
searchItem?.isVisible = true
}
LOADED_EMPTY, FAILED -> {
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.swipeRefreshLayoutView?.isEnabled = true
view?.loadingStateView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
searchItem?.isVisible = false
if (value.equals(FAILED)) {
view?.stateWithMessageView?.errorStateTextView?.text = messageData
if (messageData.equals(
context.resources.getString(R.string.nc_no_connection_error)
)
) {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
drawable.ic_signal_wifi_off_white_24dp
)
} else {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
drawable.ic_announcement_white_24dp
)
}
view?.floatingActionButton?.visibility = View.GONE
} else {
view?.floatingActionButton?.visibility = View.VISIBLE
view?.stateWithMessageView?.errorStateTextView?.text =
resources?.getText(R.string.nc_conversations_empty)
view?.stateWithMessageView?.errorStateImageView?.setImageResource(drawable.ic_logo)
}
view?.stateWithMessageView?.visibility = View.VISIBLE
}
else -> {
// We should not be here
}
}
})
conversationsLiveData.observe(this@ConversationsListView, Observer {
if (it.isEmpty()) {
viewState.value = LOADED_EMPTY
if (viewState.value != LOADED_EMPTY) {
viewState.value = LOADED_EMPTY
}
} else {
viewState.value = LOADED
if (viewState.value != LOADED) {
viewState.value = LOADED
}
}
val newConversations = mutableListOf<ConversationItem>()
@ -297,6 +248,67 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
})
}
viewState.observe(this@ConversationsListView, Observer { value ->
when (value) {
LOADING -> {
view?.swipeRefreshLayoutView?.isEnabled = false
view?.loadingStateView?.visibility = View.VISIBLE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
view?.floatingActionButton?.visibility = View.GONE
searchItem?.isVisible = false
}
LOADED -> {
view?.swipeRefreshLayoutView?.isEnabled = true
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.loadingStateView?.visibility = View.GONE
view?.stateWithMessageView?.visibility = View.GONE
view?.dataStateView?.visibility = View.VISIBLE
view?.floatingActionButton?.visibility = View.VISIBLE
searchItem?.isVisible = true
}
LOADED_EMPTY, FAILED -> {
view?.swipeRefreshLayoutView?.post {
view?.swipeRefreshLayoutView?.isRefreshing = false
}
view?.swipeRefreshLayoutView?.isEnabled = true
view?.loadingStateView?.visibility = View.GONE
view?.dataStateView?.visibility = View.GONE
searchItem?.isVisible = false
if (value.equals(FAILED)) {
view?.stateWithMessageView?.errorStateTextView?.text = viewModel.messageData
if (viewModel.messageData.equals(
context.resources.getString(R.string.nc_no_connection_error)
)
) {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
drawable.ic_signal_wifi_off_white_24dp
)
} else {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
drawable.ic_announcement_white_24dp
)
}
view?.floatingActionButton?.visibility = View.GONE
} else {
view?.floatingActionButton?.visibility = View.VISIBLE
view?.stateWithMessageView?.errorStateTextView?.text =
resources?.getText(R.string.nc_conversations_empty)
view?.stateWithMessageView?.errorStateImageView?.setImageResource(drawable.ic_logo)
}
view?.stateWithMessageView?.visibility = View.VISIBLE
}
else -> {
// We should not be here
}
}
})
return super.onCreateView(inflater, container)
}
@ -311,7 +323,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
@OnClick(R.id.stateWithMessageView)
fun onStateWithMessageViewClick() {
if (viewModel.viewState.equals(LOADED_EMPTY)) {
if (viewState.value!! == LOADED_EMPTY) {
openNewConversationScreen()
}
}

View File

@ -41,7 +41,6 @@ import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.SetConversationFavoriteValueUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListViewState.LOADING
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.ShareUtils
import kotlinx.coroutines.launch
@ -57,14 +56,11 @@ class ConversationsListViewModel constructor(
usersRepository: UsersRepository
) : BaseViewModel<ConversationsListView>(application) {
val viewState = MutableLiveData(LOADING)
var messageData: String? = null
val searchQuery = MutableLiveData<String>()
val currentUserLiveData: LiveData<UserNgEntity> = usersRepository.getActiveUserLiveData()
val conversationsLiveData = Transformations.switchMap(currentUserLiveData) {
if (viewState.value != LOADING) {
viewState.value = LOADING
}
loadConversations()
conversationsRepository.getConversationsForUser(it.id!!)
}

View File

@ -21,6 +21,7 @@
package com.nextcloud.talk.newarch.features.conversationsList
enum class ConversationsListViewState {
INITIAL_LOAD,
LOADING,
LOADED_EMPTY,
LOADED,

View File

@ -20,6 +20,7 @@
package com.nextcloud.talk.newarch.local.models
import android.util.Log
import androidx.room.*
import androidx.room.ForeignKey.CASCADE
import com.nextcloud.talk.models.json.chat.ChatMessage
@ -42,7 +43,7 @@ import java.util.*
)
data class ConversationEntity(
@PrimaryKey @ColumnInfo(name = "id") var id: String,
@ColumnInfo(name = "user_id") var user: Long? = null,
@ColumnInfo(name = "user_id") var userId: Long? = null,
@ColumnInfo(name = "conversation_id") var conversationId: String? = null,
@ColumnInfo(name = "token") var token: String? = null,
@ColumnInfo(name = "name") var name: String? = null,
@ -74,11 +75,52 @@ data class ConversationEntity(
@ColumnInfo(name = "last_read_message") var lastReadMessageId: Long = 0,
@ColumnInfo(name = "modified_at") var modifiedAt: Long? = null,
@ColumnInfo(name = "changing") var changing: Boolean = false
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ConversationEntity
if (id != other.id) return false
if (userId != other.userId) return false
if (conversationId != other.conversationId) return false
if (token != other.token) return false
if (name != other.name) return false
if (displayName != other.displayName) return false
if (type != other.type) return false
if (count != other.count) return false
if (numberOfGuests != other.numberOfGuests) return false
if (participantsCount != other.participantsCount) return false
if (participantType != other.participantType) return false
if (hasPassword != other.hasPassword) return false
if (sessionId != other.sessionId) return false
if (favorite != other.favorite) return false
if (lastActivity != other.lastActivity) return false
if (unreadMessages != other.unreadMessages) return false
if (unreadMention != other.unreadMention) return false
if (lastMessage?.internalMessageId != other.lastMessage?.internalMessageId) return false
if (objectType != other.objectType) return false
if (notificationLevel != other.notificationLevel) return false
if (conversationReadOnlyState != other.conversationReadOnlyState) return false
if (lobbyState != other.lobbyState) return false
if (lobbyTimer != other.lobbyTimer) return false
if (lastReadMessageId != other.lastReadMessageId) return false
if (changing != other.changing) return false
return true
}
override fun hashCode(): Int {
var result = userId?.hashCode() ?: 0
result = 31 * result + (token?.hashCode() ?: 0)
return result
}
}
fun ConversationEntity.toConversation(): Conversation {
val conversation = Conversation()
conversation.internalUserId = this.user
conversation.internalUserId = this.userId
conversation.conversationId = this.conversationId
conversation.type = this.type
conversation.token = this.token
@ -111,7 +153,7 @@ fun ConversationEntity.toConversation(): Conversation {
fun Conversation.toConversationEntity(): ConversationEntity {
val conversationEntity = ConversationEntity(this.internalUserId.toString() + "@" + this.token)
conversationEntity.user = this.internalUserId
conversationEntity.userId = this.internalUserId
conversationEntity.conversationId = this.conversationId
conversationEntity.token = this.token
conversationEntity.name = this.name

View File

@ -39,7 +39,7 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType
)
data class MessageEntity(
@PrimaryKey @ColumnInfo(name = "id") var id: String,
@ColumnInfo(name = "conversation_id") var conversation: String,
@ColumnInfo(name = "conversation_id") var conversationId: String,
@ColumnInfo(name = "message_id") var messageId: Long = 0,
@ColumnInfo(name = "actor_id") var actorId: String? = null,
@ColumnInfo(name = "actor_type") var actorType: String? = null,
@ -56,7 +56,7 @@ data class MessageEntity(
fun MessageEntity.toChatMessage(): ChatMessage {
val chatMessage = ChatMessage()
chatMessage.internalMessageId = this.id
chatMessage.internalConversationId = this.conversation
chatMessage.internalConversationId = this.conversationId
chatMessage.jsonMessageId = this.messageId
chatMessage.actorType = this.actorType
chatMessage.actorId = this.actorId

View File

@ -0,0 +1,99 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 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.utils
import androidx.lifecycle.LiveData
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.JoinConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import org.koin.core.KoinComponent
import org.koin.core.parameter.parametersOf
import java.net.CookieManager
class ConversationsManager constructor(usersRepository: UsersRepository,
cookieManager: CookieManager,
okHttpClient: OkHttpClient,
private val conversationsRepository: ConversationsRepository,
private val joinConversationUseCase: JoinConversationUseCase,
private val getConversationUseCase: GetConversationUseCase): KoinComponent {
private val applicationScope = CoroutineScope(Dispatchers.Default)
private val previousUser: UserNgEntity? = null
private val currentUserLiveData: LiveData<UserNgEntity> = usersRepository.getActiveUserLiveData()
private var currentConversation: Conversation? = null
init {
currentUserLiveData.observeForever { user ->
cookieManager.cookieStore.removeAll()
okHttpClient.dispatcher().cancelAll()
currentConversation = null
}
}
suspend fun getConversation(conversationToken: String) {
val currentUser = currentUserLiveData.value
getConversationUseCase.invoke(applicationScope, parametersOf(
currentUser,
conversationToken
),
object : UseCaseResponse<RoomOverall> {
override suspend fun onSuccess(result: RoomOverall) {
currentUser?.let {
conversationsRepository.saveConversationsForUser(it.id!!, listOf(result.ocs.data))
}
}
override fun onError(errorModel: ErrorModel?) {
// what do we do on error
}
})
}
suspend fun joinConversation(conversationToken: String, conversationPassword: String?, conversationsManagerInterface: ConversationsManagerInterface) {
val currentUser = currentUserLiveData.value
joinConversationUseCase.invoke(applicationScope, parametersOf(
currentUser,
conversationToken,
conversationPassword
),
object : UseCaseResponse<RoomOverall> {
override suspend fun onSuccess(result: RoomOverall) {
currentUser?.let {
conversationsRepository.saveConversationsForUser(it.id!!, listOf(result.ocs.data))
currentConversation = conversationsRepository.getConversationForUserWithToken(it.id!!, result.ocs!!.data!!.token!!)
conversationsManagerInterface.joinedConversationForUser(it, currentConversation)
}
}
override fun onError(errorModel: ErrorModel?) {
// what do we do on error
}
})
}
}

View File

@ -0,0 +1,28 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 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.utils
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.local.models.UserNgEntity
interface ConversationsManagerInterface {
fun joinedConversationForUser(userNgEntity: UserNgEntity, conversation: Conversation?)
}

View File

@ -51,9 +51,8 @@ class Images {
target(target)
}
if (userEntity != null && url.startsWith(userEntity.baseUrl) && url.contains(
"index.php/core/preview?fileId="
)
if (userEntity != null && url.startsWith(userEntity.baseUrl) && (url.contains(
"index.php/core/preview?fileId=") || url.contains("index.php/avatar/"))
) {
addHeader("Authorization", userEntity.getCredentials())
}

View File

@ -0,0 +1,62 @@
package com.nextcloud.talk.newarch.utils
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import java.util.*
import kotlin.collections.ArrayList
class LiveDataTransformations {
/**
* Creates a new [LiveData] object that does not emit a value until the source LiveData
* value has been changed. The value is considered changed if `equals()` yields
* `false`.
*
* @param source the input [LiveData]
* @param <X> the generic type parameter of `source`
* @return a new [LiveData] of type `X`
</X> */
@MainThread
fun <X> distinctUntilChangedForArray(source: LiveData<X>): LiveData<X> {
val outputLiveData = MediatorLiveData<X>()
outputLiveData.addSource(source, object : Observer<X> {
var mFirstTime = true
override fun onChanged(currentValue: X?) {
val previousValue = outputLiveData.value
if (mFirstTime && currentValue != null
|| (previousValue == null && currentValue != null)) {
mFirstTime = false
outputLiveData.value = currentValue
} else if (previousValue != null) {
if (currentValue == null) {
outputLiveData.value = currentValue
} else {
val previousArray: Array<X> = (previousValue as ArrayList<X>).toArray() as Array<X>
val currentArray: Array<X> = (currentValue as ArrayList<X>).toArray() as Array<X>
if (previousArray.size != currentArray.size) {
outputLiveData.value = currentValue
} else {
var counter = 0
for(element in previousArray) {
if (!element!!.equals(currentArray[counter])) {
outputLiveData.value = currentValue
return
}
counter++
}
}
}
}
}
})
return outputLiveData
}
}

View File

@ -262,7 +262,7 @@ object DisplayUtils {
)
}
val request = Images().getRequestForUrl(Coil.loader(), context, url, null, target, null, CircleCropTransformation())
val request = Images().getRequestForUrl(Coil.loader(), context, url, conversationUser, target, null, CircleCropTransformation())
Coil.loader().load(request)
}