From 817ea1ab6400288d8f9d3f68a7a02c20e5f95e19 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 21 Jun 2023 17:18:36 +0200 Subject: [PATCH] Avoid to send conversation and user via intent sending too much data via intent always is a bad pattern which can lead to TransactionTooLargeException. When OpenAI translation is enabled, the capabilities contain a ton of translation combinations. These capabilities are contained in 'currentUser' as well in 'selectedConversation'. So, TransactionTooLargeException was thrown. this PR: - avoids passing too much data as parcelables in intents (esp. conversation and user) - introduces MVVM patterns to load required data (esp conversation) from backend (for now via requests, in the future from database first) - introduces ConversationModel which is created out of the Conversation json model - loads user data via injection when possible - creates some quickfixes in ConversationBottomDialog, EntryMenuController and OperationsMenuController. Signed-off-by: Marcel Hibbe --- app/src/main/AndroidManifest.xml | 2 +- .../talk/activities/CallActivity.java | 11 +- .../talk/activities/CallBaseActivity.java | 8 +- .../nextcloud/talk/activities/MainActivity.kt | 47 +- .../CallNotificationActivity.kt | 198 ++++---- .../viewmodel/CallNotificationViewModel.kt | 79 +++ .../com/nextcloud/talk/chat/ChatActivity.kt | 454 +++++++----------- .../talk/chat/data/ChatRepository.kt | 31 ++ .../talk/chat/data/ChatRepositoryImpl.kt | 57 +++ .../talk/chat/viewmodels/ChatViewModel.kt | 116 +++++ .../talk/contacts/ContactsActivity.kt | 46 +- .../bottomsheet/EntryMenuController.kt | 212 ++++---- .../bottomsheet/OperationsMenuController.kt | 114 ++--- .../ConversationInfoActivity.kt | 15 +- .../ConversationInfoEditActivity.kt | 169 +++---- .../data/ConversationInfoEditRepository.kt | 33 ++ .../ConversationInfoEditRepositoryImpl.kt | 70 +++ .../ConversationInfoEditViewModel.kt | 141 ++++++ .../ConversationsListActivity.kt | 66 ++- .../talk/dagger/modules/RepositoryModule.kt | 16 + .../talk/dagger/modules/ViewModelModule.kt | 20 +- .../talk/data/source/local/TalkDatabase.kt | 1 + .../talk/extensions/ImageViewExtensions.kt | 25 +- .../nextcloud/talk/jobs/NotificationWorker.kt | 10 +- .../talk/jobs/UploadAndShareFilesWorker.kt | 4 +- .../talk/models/domain/ConversationModel.kt | 136 ++++++ .../models/json/conversations/Conversation.kt | 9 + .../ListOpenConversationsActivity.kt | 3 - .../talk/polls/ui/PollMainDialogFragment.kt | 9 +- .../receivers/ShareRecordingToChatReceiver.kt | 5 - .../reactions/ReactionsRepository.kt | 5 +- .../reactions/ReactionsRepositoryImpl.kt | 9 +- .../activities/SharedItemsActivity.kt | 9 +- .../ui/bottom/sheet/ProfileBottomSheet.kt | 42 +- .../dialog/ChooseAccountDialogFragment.java | 2 +- .../dialog/ConversationsListBottomDialog.kt | 17 +- .../talk/ui/dialog/MessageActionsDialog.kt | 14 +- .../talk/ui/dialog/SetStatusDialogFragment.kt | 9 +- .../talk/ui/dialog/ShowReactionsDialog.kt | 7 +- .../nextcloud/talk/utils/ConversationUtils.kt | 89 ++++ .../talk/utils/ParticipantPermissions.kt | 9 +- .../nextcloud/talk/utils/bundle/BundleKeys.kt | 6 +- 42 files changed, 1455 insertions(+), 870 deletions(-) rename app/src/main/java/com/nextcloud/talk/{activities => callnotification}/CallNotificationActivity.kt (75%) create mode 100644 app/src/main/java/com/nextcloud/talk/callnotification/viewmodel/CallNotificationViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5e8cf247..e8b9bb517 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -140,7 +140,7 @@ android:theme="@style/AppTheme.CallLauncher" /> = Build.VERSION_CODES.O; } - abstract void updateUiForPipMode(); + public abstract void updateUiForPipMode(); - abstract void updateUiForNormalMode(); + public abstract void updateUiForNormalMode(); - abstract void suppressFitsSystemWindows(); + public abstract void suppressFitsSystemWindows(); } diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 78f4d3535..613358827 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -47,6 +47,7 @@ import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.callnotification.CallNotificationActivity import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.controllers.ServerSelectionController import com.nextcloud.talk.controllers.WebViewLoginController @@ -61,16 +62,13 @@ import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ACCOUNT -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import io.reactivex.Observer import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.parceler.Parcels import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -282,45 +280,12 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, currentUser) bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - // FIXME once APIv2 or later is used only, the createRoom already returns all the data - ncApi.getRoom( - credentials, - ApiUtils.getUrlForRoom( - apiVersion, - currentUser?.baseUrl, - roomOverall.ocs!!.data!!.token - ) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - bundle.putParcelable( - KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data) - ) - - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - startActivity(chatIntent) - } - - override fun onError(e: Throwable) { - // unused atm - } - - override fun onComplete() { - // unused atm - } - }) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + startActivity(chatIntent) } override fun onError(e: Throwable) { @@ -337,7 +302,9 @@ class MainActivity : BaseActivity(), ActionBarProvider { super.onNewIntent(intent) Log.d(TAG, "onNewIntent Activity: " + System.identityHashCode(this).toString()) - val user = intent.getParcelableExtra(KEY_USER_ENTITY) + val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + val user = userManager.getUserWithId(internalUserId!!).blockingGet() + if (user != null && userManager.setUserAsActive(user).blockingGet()) { handleIntent(intent) } diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt similarity index 75% rename from app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt rename to app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt index 0c5df6a12..dfcbd511a 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt @@ -19,7 +19,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.nextcloud.talk.activities +package com.nextcloud.talk.callnotification import android.annotation.SuppressLint import android.content.Intent @@ -30,36 +30,36 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.View +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.app.NotificationManagerCompat +import androidx.lifecycle.ViewModelProvider import autodagger.AutoInjector import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.activities.CallBaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.callnotification.viewmodel.CallNotificationViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.CallNotificationActivityBinding import com.nextcloud.talk.extensions.loadUserAvatar -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability -import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers import okhttp3.Cache -import org.parceler.Parcels import java.io.IOException import javax.inject.Inject @@ -77,13 +77,18 @@ class CallNotificationActivity : CallBaseActivity() { @Inject lateinit var userManager: UserManager + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var callNotificationViewModel: CallNotificationViewModel + private val disposablesList: MutableList = ArrayList() private var originalBundle: Bundle? = null private var roomToken: String? = null private var notificationTimestamp: Int? = null private var userBeingCalled: User? = null private var credentials: String? = null - private var currentConversation: Conversation? = null + var currentConversation: ConversationModel? = null private var leavingScreen = false private var handler: Handler? = null private var binding: CallNotificationActivityBinding? = null @@ -98,19 +103,20 @@ class CallNotificationActivity : CallBaseActivity() { val extras = intent.extras roomToken = extras!!.getString(KEY_ROOM_TOKEN, "") notificationTimestamp = extras.getInt(BundleKeys.KEY_NOTIFICATION_TIMESTAMP) - currentConversation = Parcels.unwrap(extras.getParcelable(KEY_ROOM)) - userBeingCalled = extras.getParcelable(KEY_USER_ENTITY) + + val internalUserId = extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + userBeingCalled = userManager.getUserWithId(internalUserId).blockingGet() + originalBundle = extras credentials = ApiUtils.getCredentials(userBeingCalled!!.username, userBeingCalled!!.token) + callNotificationViewModel = ViewModelProvider(this, viewModelFactory)[CallNotificationViewModel::class.java] + + initObservers() + if (userManager.setUserAsActive(userBeingCalled!!).blockingGet()) { setCallDescriptionText() - if (currentConversation == null) { - handleFromNotification() - } else { - setUpAfterConversationIsKnown() - } - initClickListeners() + callNotificationViewModel.getRoom(userBeingCalled!!, roomToken!!) } } @@ -140,6 +146,78 @@ class CallNotificationActivity : CallBaseActivity() { binding!!.hangupButton.setOnClickListener { hangup() } } + private fun initObservers() { + val apiVersion = ApiUtils.getConversationApiVersion( + userBeingCalled, + intArrayOf( + ApiUtils.APIv4, + ApiUtils.APIv3, + 1 + ) + ) + + callNotificationViewModel.getRoomViewState.observe(this) { state -> + when (state) { + is CallNotificationViewModel.GetRoomSuccessState -> { + currentConversation = state.conversationModel + + binding!!.conversationNameTextView.text = currentConversation!!.displayName + if (currentConversation!!.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + binding!!.avatarImageView.loadUserAvatar( + userBeingCalled!!, + currentConversation!!.name!!, + true, + false + ) + } else { + binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group) + } + + val notificationHandler = Handler(Looper.getMainLooper()) + notificationHandler.post(object : Runnable { + override fun run() { + if (NotificationUtils.isNotificationVisible(context, notificationTimestamp!!.toInt())) { + notificationHandler.postDelayed(this, ONE_SECOND) + } else { + finish() + } + } + }) + + showAnswerControls() + + if (apiVersion >= ApiUtils.APIv3) { + val hasCallFlags = hasSpreedFeatureCapability( + userBeingCalled, + "conversation-call-flags" + ) + if (hasCallFlags) { + if (isInCallWithVideo(currentConversation!!.callFlag)) { + binding!!.incomingCallVoiceOrVideoTextView.text = String.format( + resources.getString(R.string.nc_call_video), + resources.getString(R.string.nc_app_product_name) + ) + } else { + binding!!.incomingCallVoiceOrVideoTextView.text = String.format( + resources.getString(R.string.nc_call_voice), + resources.getString(R.string.nc_app_product_name) + ) + } + } + } + + initClickListeners() + } + + is CallNotificationViewModel.GetRoomErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + else -> {} + } + } + } + private fun setCallDescriptionText() { val callDescriptionWithoutTypeInfo = String.format( resources.getString(R.string.nc_call_unknown), @@ -178,7 +256,7 @@ class CallNotificationActivity : CallBaseActivity() { ) originalBundle!!.putBoolean( BundleKeys.KEY_IS_MODERATOR, - currentConversation!!.isParticipantOwnerOrModerator + ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) ) val intent = Intent(this, CallActivity::class.java) @@ -189,85 +267,10 @@ class CallNotificationActivity : CallBaseActivity() { } } - @Suppress("MagicNumber") - private fun handleFromNotification() { - val apiVersion = ApiUtils.getConversationApiVersion( - userBeingCalled, - intArrayOf( - ApiUtils.APIv4, - ApiUtils.APIv3, - 1 - ) - ) - ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled!!.baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .retry(GET_ROOM_RETRY_COUNT) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposablesList.add(d) - } - - override fun onNext(roomOverall: RoomOverall) { - currentConversation = roomOverall.ocs!!.data - setUpAfterConversationIsKnown() - if (apiVersion >= 3) { - val hasCallFlags = hasSpreedFeatureCapability( - userBeingCalled, - "conversation-call-flags" - ) - if (hasCallFlags) { - if (isInCallWithVideo(currentConversation!!.callFlag)) { - binding!!.incomingCallVoiceOrVideoTextView.text = String.format( - resources.getString(R.string.nc_call_video), - resources.getString(R.string.nc_app_product_name) - ) - } else { - binding!!.incomingCallVoiceOrVideoTextView.text = String.format( - resources.getString(R.string.nc_call_voice), - resources.getString(R.string.nc_app_product_name) - ) - } - } - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, e.message, e) - } - - override fun onComplete() { - // unused atm - } - }) - } - private fun isInCallWithVideo(callFlag: Int): Boolean { return (callFlag and Participant.InCallFlags.WITH_VIDEO) > 0 } - private fun setUpAfterConversationIsKnown() { - binding!!.conversationNameTextView.text = currentConversation!!.displayName - if (currentConversation!!.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { - binding!!.avatarImageView.loadUserAvatar(userBeingCalled!!, currentConversation!!.name!!, true, false) - } else { - binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group) - } - - val notificationHandler = Handler(Looper.getMainLooper()) - notificationHandler.post(object : Runnable { - override fun run() { - if (NotificationUtils.isNotificationVisible(context, notificationTimestamp!!.toInt())) { - notificationHandler.postDelayed(this, ONE_SECOND) - } else { - finish() - } - } - }) - - showAnswerControls() - } - override fun onStop() { val notificationManager = NotificationManagerCompat.from(context) notificationManager.cancel(notificationTimestamp!!) @@ -303,23 +306,22 @@ class CallNotificationActivity : CallBaseActivity() { } } - public override fun updateUiForPipMode() { + override fun updateUiForPipMode() { binding!!.callAnswerButtons.visibility = View.INVISIBLE binding!!.incomingCallRelativeLayout.visibility = View.INVISIBLE } - public override fun updateUiForNormalMode() { + override fun updateUiForNormalMode() { binding!!.callAnswerButtons.visibility = View.VISIBLE binding!!.incomingCallRelativeLayout.visibility = View.VISIBLE } - public override fun suppressFitsSystemWindows() { + override fun suppressFitsSystemWindows() { binding!!.controllerCallNotificationLayout.fitsSystemWindows = false } companion object { const val TAG = "CallNotificationActivity" - const val GET_ROOM_RETRY_COUNT: Long = 3 const val ONE_SECOND: Long = 1000 } } diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/viewmodel/CallNotificationViewModel.kt b/app/src/main/java/com/nextcloud/talk/callnotification/viewmodel/CallNotificationViewModel.kt new file mode 100644 index 000000000..f09dfc435 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/callnotification/viewmodel/CallNotificationViewModel.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.callnotification.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class CallNotificationViewModel @Inject constructor(private val repository: ChatRepository) : + ViewModel() { + + sealed interface ViewState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _getRoomViewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val getRoomViewState: LiveData + get() = _getRoomViewState + + fun getRoom(user: User, token: String) { + _getRoomViewState.value = GetRoomStartState + repository.getRoom(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetRoomObserver()) + } + + inner class GetRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _getRoomViewState.value = GetRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _getRoomViewState.value = GetRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = CallNotificationViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 0c0573fbb..6dec5f225 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -48,7 +48,6 @@ import android.os.Build import android.os.Bundle import android.os.CountDownTimer import android.os.Handler -import android.os.Parcelable import android.os.SystemClock import android.provider.ContactsContract import android.provider.MediaStore @@ -85,6 +84,7 @@ import androidx.core.text.bold import androidx.core.widget.doAfterTextChanged import androidx.emoji2.text.EmojiCompat import androidx.emoji2.widget.EmojiTextView +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -130,6 +130,7 @@ import com.nextcloud.talk.adapters.messages.VoiceMessageInterface import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback +import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.conversationinfo.ConversationInfoActivity import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User @@ -142,15 +143,18 @@ import com.nextcloud.talk.jobs.ShareOperationWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.location.LocationPickerActivity import com.nextcloud.talk.messagesearch.MessageSearchActivity +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationReadOnlyState +import com.nextcloud.talk.models.domain.ConversationType +import com.nextcloud.talk.models.domain.LobbyState +import com.nextcloud.talk.models.domain.ObjectType import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatOverall import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage import com.nextcloud.talk.models.json.chat.ReadStatus -import com.nextcloud.talk.models.json.conversations.Conversation 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 com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.models.json.signaling.NCSignalingMessage @@ -170,6 +174,7 @@ import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ContactUtils +import com.nextcloud.talk.utils.ConversationUtils import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils @@ -181,7 +186,6 @@ import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.VibrationUtils import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS @@ -193,8 +197,8 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil import com.nextcloud.talk.utils.rx.DisposableSet import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder @@ -215,7 +219,6 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.parceler.Parcels import retrofit2.HttpException import retrofit2.Response import java.io.File @@ -248,6 +251,9 @@ class ChatActivity : @Inject lateinit var ncApi: NcApi + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + @Inject lateinit var reactionsRepository: ReactionsRepository @@ -257,6 +263,11 @@ class ChatActivity : @Inject lateinit var dateUtils: DateUtils + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var chatViewModel: ChatViewModel + override val view: View get() = binding.root @@ -267,7 +278,7 @@ class ChatActivity : var conversationUser: User? = null private var roomPassword: String = "" var credentials: String? = null - var currentConversation: Conversation? = null + var currentConversation: ConversationModel? = null private var globalLastKnownFutureMessageId = -1 private var globalLastKnownPastMessageId = -1 var adapter: TalkMessagesListAdapter? = null @@ -383,14 +394,17 @@ class ChatActivity : setContentView(binding.root) setupSystemColors() + conversationUser = currentUserProvider.currentUser.blockingGet() + handleIntent(intent) + chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] + binding.progressBar.visibility = View.VISIBLE - initAdapter() - binding.messagesListView.setAdapter(adapter) - onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + initObservers() } override fun onNewIntent(intent: Intent) { @@ -415,7 +429,6 @@ class ChatActivity : private fun handleIntent(intent: Intent) { val extras: Bundle? = intent.extras - conversationUser = extras?.getParcelable(KEY_USER_ENTITY) roomId = extras?.getString(KEY_ROOM_ID).orEmpty() roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty() @@ -426,11 +439,6 @@ class ChatActivity : Log.d(TAG, " roomToken was null or empty!") } - if (intent.hasExtra(KEY_ACTIVE_CONVERSATION)) { - currentConversation = Parcels.unwrap(extras?.getParcelable(KEY_ACTIVE_CONVERSATION)) - participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!) - } - roomPassword = extras?.getString(BundleKeys.KEY_CONVERSATION_PASSWORD).orEmpty() credentials = if (conversationUser?.userId == "?") { @@ -455,6 +463,106 @@ class ChatActivity : active = false } + private fun initObservers() { + chatViewModel.getRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.GetRoomSuccessState -> { + currentConversation = state.conversationModel + logConversationInfos("GetRoomSuccessState") + + if (adapter == null) { + initAdapter() + binding.messagesListView.setAdapter(adapter) + } + + layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? + + loadAvatarForStatusBar() + setActionBarTitle() + participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!) + + setupSwipeToReply() + setupMentionAutocomplete() + checkShowCallButtons() + checkShowMessageInputView() + checkLobbyState() + + if (!validSessionId()) { + joinRoomWithPassword() + } else { + Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped") + } + + val delayForRecursiveCall = if (shouldShowLobby()) { + GET_ROOM_INFO_DELAY_LOBBY + } else { + GET_ROOM_INFO_DELAY_NORMAL + } + + if (getRoomInfoTimerHandler == null) { + getRoomInfoTimerHandler = Handler() + } + getRoomInfoTimerHandler?.postDelayed( + { + chatViewModel.getRoom(conversationUser!!, roomToken) + }, + delayForRecursiveCall + ) + } + + is ChatViewModel.GetRoomErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.joinRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.JoinRoomSuccessState -> { + currentConversation = state.conversationModel + + sessionIdAfterRoomJoined = currentConversation!!.sessionId + ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation!!.sessionId + ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = currentConversation!!.roomId + ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = currentConversation!!.token + ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser + + logConversationInfos("joinRoomWithPassword#onNext") + + if (isFirstMessagesProcessing) { + pullChatMessages(false) + } else { + pullChatMessages(true, false) + } + + if (webSocketInstance != null) { + webSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + sessionIdAfterRoomJoined + ) + } + if (startCallFromNotification != null && startCallFromNotification ?: false) { + startCallFromNotification = false + startACall(voiceOnly, false) + } + + if (startCallFromRoomSwitch) { + startCallFromRoomSwitch = false + startACall(voiceOnly, true) + } + } + + is ChatViewModel.JoinRoomErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + else -> {} + } + } + } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onResume() { super.onResume() @@ -484,18 +592,12 @@ class ChatActivity : cancelNotificationsForCurrentConversation() - if (TextUtils.isEmpty(roomToken)) { - handleFromNotification() - } else { - getRoomInfo() - } + chatViewModel.getRoom(conversationUser!!, roomToken) actionBar?.show() setupSwipeToReply() - layoutManager = binding?.messagesListView?.layoutManager as LinearLayoutManager? - binding?.popupBubbleView?.setRecyclerView(binding?.messagesListView) binding?.popupBubbleView?.setPopupBubbleListener { context -> @@ -632,10 +734,8 @@ class ChatActivity : binding?.messageInputView?.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) } - if (currentConversation != null && currentConversation?.roomId != null) { - loadAvatarForStatusBar() - setActionBarTitle() - } + loadAvatarForStatusBar() + setActionBarTitle() viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar) } @@ -694,8 +794,11 @@ class ChatActivity : val messageHolders = MessageHolders() val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!) - val payload = - MessagePayload(roomToken!!, currentConversation?.isParticipantOwnerOrModerator, profileBottomSheet) + val payload = MessagePayload( + roomToken, + ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!), + profileBottomSheet + ) messageHolders.setIncomingTextConfig( IncomingTextMessageViewHolder::class.java, @@ -1080,68 +1183,6 @@ class ChatActivity : !CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!) } - private fun getRoomInfo() { - logConversationInfos("getRoomInfo") - - conversationUser?.let { - val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1)) - - val startNanoTime = System.nanoTime() - Log.d(TAG, "getRoomInfo - getRoom - calling: $startNanoTime") - ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, it.baseUrl, roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposables.add(d) - } - - @Suppress("Detekt.TooGenericExceptionCaught") - override fun onNext(roomOverall: RoomOverall) { - Log.d(TAG, "getRoomInfo - getRoom - got response: $startNanoTime") - currentConversation = roomOverall.ocs!!.data - - logConversationInfos("getRoomInfo#onNext") - - loadAvatarForStatusBar() - setActionBarTitle() - participantPermissions = ParticipantPermissions(it, currentConversation!!) - - setupSwipeToReply() - setupMentionAutocomplete() - checkShowCallButtons() - checkShowMessageInputView() - checkLobbyState() - - if (!validSessionId()) { - joinRoomWithPassword() - } else { - Log.d(TAG, "already inConversation. joinRoomWithPassword is skipped") - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, "getRoomInfo - getRoom - ERROR", e) - } - - override fun onComplete() { - Log.d(TAG, "getRoomInfo - getRoom - onComplete: $startNanoTime") - - val delayForRecursiveCall = if (shouldShowLobby()) { - GET_ROOM_INFO_DELAY_LOBBY - } else { - GET_ROOM_INFO_DELAY_NORMAL - } - - if (getRoomInfoTimerHandler == null) { - getRoomInfoTimerHandler = Handler() - } - getRoomInfoTimerHandler?.postDelayed({ getRoomInfo() }, delayForRecursiveCall) - } - }) - } - } - private fun setupSwipeToReply() { if (this::participantPermissions.isInitialized && participantPermissions.hasChatPermission() && @@ -1162,46 +1203,11 @@ class ChatActivity : } } - private fun handleFromNotification() { - var apiVersion = 1 - // FIXME Can this be called for guests? - if (conversationUser != null) { - apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1)) + private fun loadAvatarForStatusBar() { + if (currentConversation == null) { + return } - Log.d(TAG, "handleFromNotification - getRooms - calling") - ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl), false) - ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposables.add(d) - } - - override fun onNext(roomsOverall: RoomsOverall) { - Log.d(TAG, "handleFromNotification - getRooms - got response") - for (conversation in roomsOverall.ocs!!.data!!) { - if (roomId == conversation.roomId) { - roomToken = conversation.token!! - currentConversation = conversation - participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!) - setActionBarTitle() - getRoomInfo() - break - } - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, "handleFromNotification - getRooms - ERROR: ", e) - } - - override fun onComplete() { - // unused atm - } - }) - } - - private fun loadAvatarForStatusBar() { if (isOneToOneConversation()) { var url = ApiUtils.getUrlForAvatar( conversationUser!!.baseUrl, @@ -1254,18 +1260,18 @@ class ChatActivity : } fun isOneToOneConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL private fun isGroupConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType.ROOM_GROUP_CALL + currentConversation?.type == ConversationType.ROOM_GROUP_CALL private fun isPublicConversation() = currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType.ROOM_PUBLIC_CALL + currentConversation?.type == ConversationType.ROOM_PUBLIC_CALL private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) { if (conversationUser != null) { runOnUiThread { - if (currentConversation?.objectType == Conversation.ObjectType.ROOM) { + if (currentConversation?.objectType == ObjectType.ROOM) { Toast.makeText( context, context.resources.getString(R.string.switch_to_main_room), @@ -1281,7 +1287,6 @@ class ChatActivity : } val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(KEY_ROOM_TOKEN, token) if (startCallAfterRoomSwitch) { @@ -1697,8 +1702,8 @@ class ChatActivity : private fun shouldShowLobby(): Boolean { if (currentConversation != null) { return CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") && - currentConversation?.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY && - currentConversation?.canModerate(conversationUser!!) == false && + currentConversation?.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY && + !ConversationUtils.canModerate(currentConversation!!, conversationUser!!) && !participantPermissions.canIgnoreLobby() } return false @@ -1733,12 +1738,12 @@ class ChatActivity : private fun isReadOnlyConversation(): Boolean { return currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == - Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY + ConversationReadOnlyState.CONVERSATION_READ_ONLY } private fun checkLobbyState() { if (currentConversation != null && - currentConversation?.isLobbyViewApplicable(conversationUser!!) == true + ConversationUtils.isLobbyViewApplicable(currentConversation!!, conversationUser!!) ) { if (shouldShowLobby()) { binding?.lobby?.lobbyView?.visibility = View.VISIBLE @@ -2119,7 +2124,7 @@ class ChatActivity : private fun showConversationInfoScreen() { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(KEY_ROOM_TOKEN, roomToken) bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation()) @@ -2274,65 +2279,8 @@ class ChatActivity : val startNanoTime = System.nanoTime() Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime") - ncApi.joinRoom( - credentials, - ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken), - roomPassword - ) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(RETRIES) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - disposables.add(d) - } - @Suppress("Detekt.TooGenericExceptionCaught") - override fun onNext(roomOverall: RoomOverall) { - Log.d(TAG, "joinRoomWithPassword - joinRoom - got response: $startNanoTime") - - val conversation = roomOverall.ocs!!.data!! - currentConversation = conversation - - sessionIdAfterRoomJoined = conversation.sessionId - ApplicationWideCurrentRoomHolder.getInstance().session = conversation.sessionId - ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = conversation.roomId - ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = conversation.token - ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser - - logConversationInfos("joinRoomWithPassword#onNext") - - if (isFirstMessagesProcessing) { - pullChatMessages(false) - } else { - pullChatMessages(true, false) - } - - if (webSocketInstance != null) { - webSocketInstance?.joinRoomWithRoomTokenAndSession( - roomToken!!, - sessionIdAfterRoomJoined - ) - } - if (startCallFromNotification != null && startCallFromNotification ?: false) { - startCallFromNotification = false - startACall(voiceOnly, false) - } - - if (startCallFromRoomSwitch) { - startCallFromRoomSwitch = false - startACall(voiceOnly, true) - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, "joinRoomWithPassword - joinRoom - ERROR", e) - } - - override fun onComplete() { - // unused atm - } - }) + chatViewModel.joinRoom(conversationUser!!, roomToken, roomPassword) } else { Log.d(TAG, "sessionID was valid -> skip joinRoom") @@ -2801,9 +2749,9 @@ class ChatActivity : GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0 ) chatMessage.isOneToOneConversation = - (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) + (currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) chatMessage.isFormerOneToOneConversation = - (currentConversation?.type == Conversation.ConversationType.FORMER_ONE_TO_ONE) + (currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) it.addToStart(chatMessage, shouldScroll) } } @@ -2845,9 +2793,9 @@ class ChatActivity : val chatMessage = chatMessageList[i] chatMessage.isOneToOneConversation = - currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL chatMessage.isFormerOneToOneConversation = - (currentConversation?.type == Conversation.ConversationType.FORMER_ONE_TO_ONE) + (currentConversation?.type == ConversationType.FORMER_ONE_TO_ONE) chatMessage.activeUser = conversationUser } @@ -3032,10 +2980,9 @@ class ChatActivity : val intent = Intent(this, SharedItemsActivity::class.java) intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName) intent.putExtra(KEY_ROOM_TOKEN, roomToken) - intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable) intent.putExtra( SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, - currentConversation?.isParticipantOwnerOrModerator + ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) ) startActivity(intent) } @@ -3119,12 +3066,11 @@ class ChatActivity : val bundle = Bundle() bundle.putString(KEY_ROOM_TOKEN, roomToken) bundle.putString(KEY_ROOM_ID, roomId) - bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) bundle.putString(KEY_CONVERSATION_NAME, it.displayName) bundle.putInt(KEY_RECORDING_STATE, it.callRecording) - bundle.putBoolean(KEY_IS_MODERATOR, it.isParticipantOwnerOrModerator) + bundle.putBoolean(KEY_IS_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(it)) bundle.putBoolean( BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO, participantPermissions.canPublishAudio() @@ -3141,7 +3087,7 @@ class ChatActivity : bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true) } - if (it.objectType == Conversation.ObjectType.ROOM) { + if (it.objectType == ObjectType.ROOM) { bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true) } @@ -3156,12 +3102,12 @@ class ChatActivity : override fun onClickReaction(chatMessage: ChatMessage, emoji: String) { VibrationUtils.vibrateShort(context) if (chatMessage.reactionsSelf?.contains(emoji) == true) { - reactionsRepository.deleteReaction(currentConversation!!, chatMessage, emoji) + reactionsRepository.deleteReaction(roomToken, chatMessage, emoji) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(ReactionDeletedObserver()) } else { - reactionsRepository.addReaction(currentConversation!!, chatMessage, emoji) + reactionsRepository.addReaction(roomToken, chatMessage, emoji) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(ReactionAddedObserver()) @@ -3171,7 +3117,7 @@ class ChatActivity : override fun onLongClickReactions(chatMessage: ChatMessage) { ShowReactionsDialog( this, - currentConversation, + roomToken, chatMessage, conversationUser, participantPermissions.hasChatPermission(), @@ -3339,48 +3285,15 @@ class ChatActivity : override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - // FIXME once APIv2+ is used only, the createRoom already returns all the data - ncApi.getRoom( - credentials, - ApiUtils.getUrlForRoom( - apiVersion, - conversationUser?.baseUrl, - roomOverall.ocs!!.data!!.token - ) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - bundle.putParcelable( - KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data!!) - ) - - leaveRoom { - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(chatIntent) - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, e.message, e) - } - - override fun onComplete() { - // unused atm - } - }) + leaveRoom { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } } override fun onError(e: Throwable) { @@ -3471,7 +3384,7 @@ class ChatActivity : conversationUser?.userId?.isNotEmpty() == true && conversationUser!!.userId != "?" && message.user.id.startsWith("users/") && message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId && - currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || isShowMessageDeletionButton(message) || // delete ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread @@ -3540,7 +3453,7 @@ class ChatActivity : messageTemp.isDeleted = true messageTemp.isOneToOneConversation = - currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL messageTemp.activeUser = conversationUser adapter?.update(messageTemp) @@ -3550,7 +3463,7 @@ class ChatActivity : val messageTemp = message as ChatMessage messageTemp.isOneToOneConversation = - currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL messageTemp.activeUser = conversationUser adapter?.update(messageTemp) @@ -3598,7 +3511,7 @@ class ChatActivity : val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) { true } else { - currentConversation!!.canModerate(conversationUser!!) + ConversationUtils.canModerate(currentConversation!!, conversationUser!!) } val isOlderThanSixHours = message @@ -3652,7 +3565,7 @@ class ChatActivity : @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { - if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + if (currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.name != userMentionClickEvent.userId ) { var apiVersion = 1 @@ -3683,42 +3596,21 @@ class ChatActivity : } override fun onNext(roomOverall: RoomOverall) { - val conversationIntent = Intent(context, CallActivity::class.java) val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - bundle.putBoolean(KEY_IS_MODERATOR, roomOverall.ocs!!.data!!.isParticipantOwnerOrModerator) - if (conversationUser != null) { - bundle.putParcelable( - KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data) - ) - conversationIntent.putExtras(bundle) - - leaveRoom { - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(chatIntent) - } - } else { - conversationIntent.putExtras(bundle) - startActivity(conversationIntent) - Handler().postDelayed( - { - if (!isDestroyed) { - finish() - } - }, - POP_CURRENT_CONTROLLER_DELAY - ) + leaveRoom { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) } } override fun onError(e: Throwable) { - // unused atm + Log.e(TAG, "error after clicking on user mention chip", e) + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() } override fun onComplete() { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt new file mode 100644 index 000000000..da39d25ff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.chat.data + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel + +import io.reactivex.Observable + +interface ChatRepository { + fun getRoom(user: User, roomToken: String): Observable + fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt new file mode 100644 index 000000000..51f77746e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepositoryImpl.kt @@ -0,0 +1,57 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.chat.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observable + +class ChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository { + override fun getRoom( + user: User, + roomToken: String + ): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token) + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1)) + + return ncApi.getRoom( + credentials, + ApiUtils.getUrlForRoom(apiVersion, user.baseUrl, roomToken) + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + } + + override fun joinRoom( + user: User, + roomToken: String, + roomPassword: String + ): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token) + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, 1)) + + return ncApi.joinRoom( + credentials, + ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl, roomToken), + roomPassword + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt new file mode 100644 index 000000000..3d6e5dc4c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -0,0 +1,116 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.chat.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class ChatViewModel @Inject constructor(private val repository: ChatRepository) : + ViewModel() { + + sealed interface ViewState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _getRoomViewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val getRoomViewState: LiveData + get() = _getRoomViewState + + object JoinRoomStartState : ViewState + object JoinRoomErrorState : ViewState + open class JoinRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _joinRoomViewState: MutableLiveData = MutableLiveData(JoinRoomStartState) + val joinRoomViewState: LiveData + get() = _joinRoomViewState + + fun getRoom(user: User, token: String) { + _getRoomViewState.value = GetRoomStartState + repository.getRoom(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetRoomObserver()) + } + + fun joinRoom(user: User, token: String, roomPassword: String) { + _getRoomViewState.value = JoinRoomStartState + repository.joinRoom(user, token, roomPassword) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.retry(JOIN_ROOM_RETRY_COUNT) + ?.subscribe(JoinRoomObserver()) + } + + inner class GetRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _getRoomViewState.value = GetRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _getRoomViewState.value = GetRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class JoinRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _joinRoomViewState.value = JoinRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _joinRoomViewState.value = JoinRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ChatViewModel::class.simpleName + const val JOIN_ROOM_RETRY_COUNT: Long = 3 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt index a5a9fb95b..6db3555bc 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -338,46 +338,13 @@ class ContactsActivity : override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - // FIXME once APIv2 or later is used only, the createRoom already returns all the data - ncApi.getRoom( - credentials, - ApiUtils.getUrlForRoom( - apiVersion, - currentUser!!.baseUrl, - roomOverall.ocs!!.data!!.token - ) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data!!) - ) - - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(chatIntent) - } - - override fun onError(e: Throwable) { - // unused atm - } - - override fun onComplete() { - // unused atm - } - }) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) } override fun onError(e: Throwable) { @@ -818,13 +785,8 @@ class ContactsActivity : override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data!!) - ) val chatIntent = Intent(context, ChatActivity::class.java) chatIntent.putExtras(bundle) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt index 78c71b40d..0063b2b73 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/EntryMenuController.kt @@ -23,14 +23,13 @@ */ package com.nextcloud.talk.controllers.bottomsheet -import android.content.Intent import android.content.res.ColorStateList import android.os.Bundle -import android.os.Parcelable import android.text.Editable import android.text.InputType import android.text.TextUtils import android.text.TextWatcher +import android.util.Log import android.view.View import android.view.inputmethod.EditorInfo import androidx.core.content.res.ResourcesCompat @@ -40,21 +39,28 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.google.android.material.textfield.TextInputLayout import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ControllerEntryMenuBinding import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder import com.vanniktech.emoji.EmojiPopup +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers import okhttp3.internal.immutableListOf import org.greenrobot.eventbus.EventBus -import org.parceler.Parcels -import org.parceler.Parcels.unwrap import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -65,6 +71,9 @@ class EntryMenuController(args: Bundle) : ) { private val binding: ControllerEntryMenuBinding? by viewBinding(ControllerEntryMenuBinding::bind) + @Inject + lateinit var ncApi: NcApi + @Inject lateinit var eventBus: EventBus @@ -73,12 +82,13 @@ class EntryMenuController(args: Bundle) : private val operation: ConversationOperationEnum private var conversation: Conversation? = null - private var shareIntent: Intent? = null private val packageName: String private val name: String - private val callUrl: String + private var emojiPopup: EmojiPopup? = null private val originalBundle: Bundle + private var currentUser: User? = null + private val roomToken: String override val appBarLayoutType: AppBarLayoutType get() = AppBarLayoutType.SEARCH_BAR @@ -100,74 +110,121 @@ class EntryMenuController(args: Bundle) : override fun onViewBound(view: View) { super.onViewBound(view) - if (conversation != null && operation === ConversationOperationEnum.OPS_CODE_RENAME_ROOM) { - binding?.textEdit?.setText(conversation!!.name) - } + currentUser = userManager.currentUser.blockingGet() - binding?.textEdit?.setOnEditorActionListener { v, actionId, event -> - @Suppress("IMPLICIT_BOXING_IN_IDENTITY_EQUALS") - if (actionId === EditorInfo.IME_ACTION_DONE && binding?.okButton?.isEnabled == true) { - binding?.okButton?.callOnClick() - return@setOnEditorActionListener true - } - false - } + if (operation == ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) { + var labelText = "" + labelText = resources!!.getString(R.string.nc_conversation_link) + binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI - textEditAddChangedListener() + textEditAddChangedListener() - var labelText = "" - when (operation) { - ConversationOperationEnum.OPS_CODE_INVITE_USERS, ConversationOperationEnum.OPS_CODE_RENAME_ROOM -> { - labelText = resources!!.getString(R.string.nc_call_name) - binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT - binding?.smileyButton?.visibility = View.VISIBLE - emojiPopup = binding?.let { - EmojiPopup( - rootView = view, - editText = it.textEdit, - onEmojiPopupShownListener = { - viewThemeUtils.platform.colorImageView(it.smileyButton, ColorRole.PRIMARY) - }, - onEmojiPopupDismissListener = { - it.smileyButton.imageTintList = ColorStateList.valueOf( - ResourcesCompat.getColor(resources!!, R.color.medium_emphasis_text, context.theme) - ) - }, - onEmojiClickListener = { - binding?.textEdit?.editableText?.append(" ") - } - ) - } - } + binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) } + binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) } - ConversationOperationEnum.OPS_CODE_JOIN_ROOM -> { - // 99 is joining a conversation via password - labelText = resources!!.getString(R.string.nc_password) - binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD - } + binding?.textInputLayout?.hint = labelText + binding?.textInputLayout?.requestFocus() - ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM -> { - labelText = resources!!.getString(R.string.nc_conversation_link) - binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI - } - - else -> { - } - } - if (PASSWORD_ENTRY_OPERATIONS.contains(operation)) { - binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + binding?.smileyButton?.setOnClickListener { onSmileyClick() } + binding?.okButton?.setOnClickListener { onOkButtonClick() } } else { - binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_NONE + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1)) + ncApi.getRoom( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForRoom(apiVersion, currentUser!!.baseUrl, roomToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(1) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + @Suppress("Detekt.LongMethod") + override fun onNext(roomOverall: RoomOverall) { + conversation = roomOverall.ocs!!.data + + if (conversation != null && operation === ConversationOperationEnum.OPS_CODE_RENAME_ROOM) { + binding?.textEdit?.setText(conversation!!.name) + } + + binding?.textEdit?.setOnEditorActionListener { v, actionId, event -> + @Suppress("IMPLICIT_BOXING_IN_IDENTITY_EQUALS") + if (actionId === EditorInfo.IME_ACTION_DONE && binding?.okButton?.isEnabled == true) { + binding?.okButton?.callOnClick() + return@setOnEditorActionListener true + } + false + } + + textEditAddChangedListener() + + var labelText = "" + when (operation) { + ConversationOperationEnum.OPS_CODE_INVITE_USERS, + ConversationOperationEnum.OPS_CODE_RENAME_ROOM -> { + labelText = resources!!.getString(R.string.nc_call_name) + binding?.textEdit?.inputType = InputType.TYPE_CLASS_TEXT + binding?.smileyButton?.visibility = View.VISIBLE + emojiPopup = binding?.let { + EmojiPopup( + rootView = view, + editText = it.textEdit, + onEmojiPopupShownListener = { + viewThemeUtils.platform.colorImageView(it.smileyButton, ColorRole.PRIMARY) + }, + onEmojiPopupDismissListener = { + it.smileyButton.imageTintList = ColorStateList.valueOf( + ResourcesCompat.getColor( + resources!!, + R.color.medium_emphasis_text, + context.theme + ) + ) + }, + onEmojiClickListener = { + binding?.textEdit?.editableText?.append(" ") + } + ) + } + } + + ConversationOperationEnum.OPS_CODE_JOIN_ROOM -> { + // 99 is joining a conversation via password + labelText = resources!!.getString(R.string.nc_password) + binding?.textEdit?.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + + else -> { + } + } + if (PASSWORD_ENTRY_OPERATIONS.contains(operation)) { + binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + } else { + binding?.textInputLayout?.endIconMode = TextInputLayout.END_ICON_NONE + } + + binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) } + binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) } + + binding?.textInputLayout?.hint = labelText + binding?.textInputLayout?.requestFocus() + + binding?.smileyButton?.setOnClickListener { onSmileyClick() } + binding?.okButton?.setOnClickListener { onOkButtonClick() } + } + + override fun onError(e: Throwable) { + Log.e("EntryMenuController", "error") + } + + override fun onComplete() { + // unused atm + } + }) } - - binding?.textInputLayout?.let { viewThemeUtils.material.colorTextInputLayout(it) } - binding?.okButton?.let { viewThemeUtils.material.colorMaterialButtonText(it) } - - binding?.textInputLayout?.hint = labelText - binding?.textInputLayout?.requestFocus() - - binding?.smileyButton?.setOnClickListener { onSmileyClick() } - binding?.okButton?.setOnClickListener { onOkButtonClick() } } private fun textEditAddChangedListener() { @@ -242,7 +299,8 @@ class EntryMenuController(args: Bundle) : ) { val bundle = Bundle() conversation!!.name = binding?.textEdit?.text.toString() - bundle.putParcelable(BundleKeys.KEY_ROOM, Parcels.wrap(conversation)) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + bundle.putString(BundleKeys.KEY_NEW_ROOM_NAME, binding?.textEdit?.text.toString()) bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, operation) router.pushController( RouterTransaction.with(OperationsMenuController(bundle)) @@ -274,16 +332,9 @@ class EntryMenuController(args: Bundle) : private fun joinRoom() { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_ROOM, Parcels.wrap(conversation)) - bundle.putString(BundleKeys.KEY_CALL_URL, callUrl) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, binding?.textEdit?.text.toString()) bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, operation) - if (originalBundle.containsKey(BundleKeys.KEY_SERVER_CAPABILITIES)) { - bundle.putParcelable( - BundleKeys.KEY_SERVER_CAPABILITIES, - originalBundle.getParcelable(BundleKeys.KEY_SERVER_CAPABILITIES) - ) - } router.pushController( RouterTransaction.with(OperationsMenuController(bundle)) .pushChangeHandler(HorizontalChangeHandler()) @@ -296,15 +347,10 @@ class EntryMenuController(args: Bundle) : originalBundle = args operation = args.getSerializable(BundleKeys.KEY_OPERATION_CODE) as ConversationOperationEnum - if (args.containsKey(BundleKeys.KEY_ROOM)) { - conversation = unwrap(args.getParcelable(BundleKeys.KEY_ROOM)) - } - if (args.containsKey(BundleKeys.KEY_SHARE_INTENT)) { - shareIntent = unwrap(args.getParcelable(BundleKeys.KEY_SHARE_INTENT)) - } + roomToken = args.getString(KEY_ROOM_TOKEN, "") name = args.getString(BundleKeys.KEY_APP_ITEM_NAME, "") packageName = args.getString(BundleKeys.KEY_APP_ITEM_PACKAGE_NAME, "") - callUrl = args.getString(BundleKeys.KEY_CALL_URL, "") + // callUrl = args.getString(BundleKeys.KEY_CALL_URL, "") } companion object { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt index 0c98fcc43..e6b083710 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.kt @@ -21,8 +21,6 @@ */ package com.nextcloud.talk.controllers.bottomsheet -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.text.TextUtils import android.util.Log @@ -42,7 +40,6 @@ import com.nextcloud.talk.databinding.ControllerOperationsMenuBinding import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.events.OpenConversationEvent import com.nextcloud.talk.models.RetrofitBucket -import com.nextcloud.talk.models.json.capabilities.Capabilities import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType @@ -53,19 +50,17 @@ import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.NoSupportedApiException -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION +import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_TYPE import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INVITED_GROUP import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INVITED_PARTICIPANTS +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_ROOM_NAME import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPERATION_CODE -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SERVER_CAPABILITIES -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder import io.reactivex.Observer @@ -100,13 +95,14 @@ class OperationsMenuController(args: Bundle) : BaseController( private var currentUser: User? = null private val callPassword: String private val callUrl: String + private var roomToken: String + private val roomNameNew: String private var baseUrl: String? = null private var conversationToken: String? = null private var disposable: Disposable? = null private var conversationType: ConversationType? = null private var invitedUsers: ArrayList? = ArrayList() private var invitedGroups: ArrayList? = ArrayList() - private var serverCapabilities: Capabilities? = null private var credentials: String? = null private val conversationName: String @@ -128,42 +124,44 @@ class OperationsMenuController(args: Bundle) : BaseController( baseUrl = callUrl.substring(0, callUrl.indexOf("/call")) } } - if (!TextUtils.isEmpty(baseUrl) && baseUrl != currentUser!!.baseUrl) { - if (serverCapabilities != null) { - try { - useBundledCapabilitiesForGuest() - } catch (e: IOException) { - // Fall back to fetching capabilities again - fetchCapabilitiesForGuest() - } - } else { - fetchCapabilitiesForGuest() - } + + if (roomToken.isNotEmpty()) { + val apiVersion = apiVersion() + ncApi.getRoom( + credentials, + ApiUtils.getUrlForRoom(apiVersion, currentUser!!.baseUrl, roomToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(1) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposable = d + } + + override fun onNext(roomOverall: RoomOverall) { + conversation = roomOverall.ocs!!.data + + if (!TextUtils.isEmpty(baseUrl) && baseUrl != currentUser!!.baseUrl) { + fetchCapabilitiesForGuest() + } else { + processOperation() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "error while fetching room", e) + } + + override fun onComplete() { + // unused atm + } + }) } else { processOperation() } } - @Throws(IOException::class) - private fun useBundledCapabilitiesForGuest() { - currentUser = User() - currentUser!!.baseUrl = baseUrl - currentUser!!.userId = "?" - try { - currentUser!!.capabilities = serverCapabilities - } catch (e: IOException) { - Log.e("OperationsMenu", "Failed to serialize capabilities") - throw e - } - try { - checkCapabilities(currentUser!!) - processOperation() - } catch (e: NoSupportedApiException) { - showResultImage(everythingOK = false, isGuestSupportError = false) - Log.d(TAG, "No supported server version found", e) - } - } - private fun fetchCapabilitiesForGuest() { ncApi.getCapabilities(null, ApiUtils.getUrlForCapabilities(baseUrl)) .subscribeOn(Schedulers.io()) @@ -218,6 +216,7 @@ class OperationsMenuController(args: Bundle) : BaseController( ConversationOperationEnum.OPS_CODE_MARK_AS_UNREAD -> operationMarkAsUnread() ConversationOperationEnum.OPS_CODE_REMOVE_FAVORITE, ConversationOperationEnum.OPS_CODE_ADD_FAVORITE -> operationToggleFavorite() + ConversationOperationEnum.OPS_CODE_JOIN_ROOM -> operationJoinRoom() else -> { } @@ -287,7 +286,7 @@ class OperationsMenuController(args: Bundle) : BaseController( currentUser!!.baseUrl, conversation!!.token ), - conversation!!.name + roomNameNew ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -427,17 +426,7 @@ class OperationsMenuController(args: Bundle) : BaseController( if (conversation!!.hasPassword && conversation!!.isGuest) { eventBus.post(ConversationsListFetchDataEvent()) val bundle = Bundle() - bundle.putParcelable(KEY_ROOM, Parcels.wrap(conversation)) - bundle.putString(KEY_CALL_URL, callUrl) - try { - bundle.putParcelable( - KEY_SERVER_CAPABILITIES, - Parcels.wrap(currentUser!!.capabilities) - ) - } catch (e: IOException) { - Log.e(TAG, "Failed to parse capabilities for guest") - showResultImage(everythingOK = false, isGuestSupportError = false) - } + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) bundle.putSerializable(KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_JOIN_ROOM) router.pushController( RouterTransaction.with(EntryMenuController(bundle)) @@ -519,16 +508,7 @@ class OperationsMenuController(args: Bundle) : BaseController( binding?.resultTextView?.setText(R.string.nc_all_ok_operation) } else { binding?.resultTextView?.setTextColor(resources!!.getColor(R.color.nc_darkRed, null)) - if (!isGuestSupportError) { - binding?.resultTextView?.setText(R.string.nc_failed_to_perform_operation) - } else { - binding?.resultTextView?.setText(R.string.nc_failed_signaling_settings) - binding?.webButton?.setOnClickListener { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(callUrl)) - startActivity(browserIntent) - } - binding?.webButton?.visibility = View.VISIBLE - } + binding?.resultTextView?.setText(R.string.nc_failed_to_perform_operation) } binding?.resultTextView?.visibility = View.VISIBLE if (everythingOK) { @@ -608,6 +588,7 @@ class OperationsMenuController(args: Bundle) : BaseController( override fun onSubscribe(d: Disposable) { // unused atm } + override fun onNext(addParticipantOverall: AddParticipantOverall) { // unused atm } @@ -653,6 +634,7 @@ class OperationsMenuController(args: Bundle) : BaseController( override fun onSubscribe(d: Disposable) { // unused atm } + override fun onNext(addParticipantOverall: AddParticipantOverall) { // unused atm } @@ -679,8 +661,6 @@ class OperationsMenuController(args: Bundle) : BaseController( bundle.putString(KEY_ROOM_TOKEN, conversation!!.token) bundle.putString(KEY_ROOM_ID, conversation!!.roomId) bundle.putString(KEY_CONVERSATION_NAME, conversation!!.displayName) - bundle.putParcelable(KEY_USER_ENTITY, currentUser) - bundle.putParcelable(KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation)) bundle.putString(KEY_CONVERSATION_PASSWORD, callPassword) eventBus.post(OpenConversationEvent(conversation, bundle)) } @@ -755,11 +735,10 @@ class OperationsMenuController(args: Bundle) : BaseController( init { operation = args.getSerializable(KEY_OPERATION_CODE) as ConversationOperationEnum? - if (args.containsKey(KEY_ROOM)) { - conversation = Parcels.unwrap(args.getParcelable(KEY_ROOM)) - } callPassword = args.getString(KEY_CONVERSATION_PASSWORD, "") callUrl = args.getString(KEY_CALL_URL, "") + roomToken = args.getString(KEY_ROOM_TOKEN, "") + roomNameNew = args.getString(KEY_NEW_ROOM_NAME, "") if (args.containsKey(KEY_INVITED_PARTICIPANTS)) { invitedUsers = args.getStringArrayList(KEY_INVITED_PARTICIPANTS) } @@ -769,9 +748,6 @@ class OperationsMenuController(args: Bundle) : BaseController( if (args.containsKey(KEY_CONVERSATION_TYPE)) { conversationType = Parcels.unwrap(args.getParcelable(KEY_CONVERSATION_TYPE)) } - if (args.containsKey(KEY_SERVER_CAPABILITIES)) { - serverCapabilities = Parcels.unwrap(args.getParcelable(KEY_SERVER_CAPABILITIES)) - } conversationName = args.getString(KEY_CONVERSATION_NAME, "") } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 36a189035..f4a64a94a 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -32,7 +32,6 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.drawable.ColorDrawable import android.os.Bundle -import android.os.Parcelable import android.text.TextUtils import android.util.Log import android.view.Menu @@ -86,6 +85,7 @@ import com.nextcloud.talk.utils.DateConstants import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager @@ -95,7 +95,6 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.parceler.Parcels import java.util.Calendar import java.util.Collections import java.util.Locale @@ -110,6 +109,9 @@ class ConversationInfoActivity : @Inject lateinit var ncApi: NcApi + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + @Inject lateinit var conversationsRepository: ConversationsRepository @@ -152,7 +154,8 @@ class ConversationInfoActivity : setContentView(binding.root) setupSystemColors() - conversationUser = intent.getParcelableExtra(BundleKeys.KEY_USER_ENTITY)!! + conversationUser = currentUserProvider.currentUser.blockingGet() + conversationToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token) @@ -225,11 +228,6 @@ class ConversationInfoActivity : override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.edit) { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(conversation) - ) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken) val intent = Intent(this, ConversationInfoEditActivity::class.java) @@ -270,7 +268,6 @@ class ConversationInfoActivity : intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName) intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken) - intent.putExtra(BundleKeys.KEY_USER_ENTITY, conversationUser as Parcelable) intent.putExtra(SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, conversation?.isParticipantOwnerOrModerator) startActivity(intent) } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt index 6eecacb88..1316655e1 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -34,34 +34,31 @@ import android.view.View import android.widget.Toast import androidx.core.net.toFile import androidx.core.view.ViewCompat +import androidx.lifecycle.ViewModelProvider import autodagger.AutoInjector import com.github.dhaval2404.imagepicker.ImagePicker import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationInfoEditBinding import com.nextcloud.talk.extensions.loadConversationAvatar import com.nextcloud.talk.extensions.loadSystemAvatar import com.nextcloud.talk.extensions.loadUserAvatar -import com.nextcloud.talk.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.generic.GenericOverall -import com.nextcloud.talk.repositories.conversations.ConversationsRepository import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.PickImage import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import org.parceler.Parcels import java.io.File import javax.inject.Inject @@ -75,13 +72,18 @@ class ConversationInfoEditActivity : lateinit var ncApi: NcApi @Inject - lateinit var conversationsRepository: ConversationsRepository + lateinit var currentUserProvider: CurrentUserProviderNew - private lateinit var conversationToken: String + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var conversationInfoEditViewModel: ConversationInfoEditViewModel + + private lateinit var roomToken: String private lateinit var conversationUser: User private lateinit var credentials: String - private var conversation: Conversation? = null + private var conversation: ConversationModel? = null private lateinit var pickImage: PickImage @@ -96,12 +98,14 @@ class ConversationInfoEditActivity : val extras: Bundle? = intent.extras - conversationUser = extras?.getParcelable(BundleKeys.KEY_USER_ENTITY)!! - conversationToken = extras.getString(BundleKeys.KEY_ROOM_TOKEN)!! + conversationUser = currentUserProvider.currentUser.blockingGet() - if (conversation == null && intent.hasExtra(BundleKeys.KEY_ACTIVE_CONVERSATION)) { - conversation = Parcels.unwrap(extras.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION)) - } + roomToken = extras?.getString(BundleKeys.KEY_ROOM_TOKEN)!! + + conversationInfoEditViewModel = + ViewModelProvider(this, viewModelFactory)[ConversationInfoEditViewModel::class.java] + + conversationInfoEditViewModel.getRoom(conversationUser, roomToken) viewThemeUtils.material.colorTextInputLayout(binding.conversationNameInputLayout) viewThemeUtils.material.colorTextInputLayout(binding.conversationDescriptionInputLayout) @@ -109,21 +113,57 @@ class ConversationInfoEditActivity : credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token) pickImage = PickImage(this, conversationUser) + + initObservers() } override fun onResume() { super.onResume() + } - loadConversationAvatar() + private fun initObservers() { + conversationInfoEditViewModel.viewState.observe(this) { state -> + when (state) { + is ConversationInfoEditViewModel.GetRoomSuccessState -> { + conversation = state.conversationModel - binding.conversationName.setText(conversation!!.displayName) + binding.conversationName.setText(conversation!!.displayName) - if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) { - binding.conversationDescription.setText(conversation!!.description) - } + if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) { + binding.conversationDescription.setText(conversation!!.description) + } - if (!CapabilitiesUtilNew.isConversationDescriptionEndpointAvailable(conversationUser)) { - binding.conversationDescription.isEnabled = false + if (!CapabilitiesUtilNew.isConversationDescriptionEndpointAvailable(conversationUser)) { + binding.conversationDescription.isEnabled = false + } + + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.GetRoomErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + is ConversationInfoEditViewModel.UploadAvatarSuccessState -> { + conversation = state.conversationModel + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.UploadAvatarErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + is ConversationInfoEditViewModel.DeleteAvatarSuccessState -> { + conversation = state.conversationModel + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.DeleteAvatarErrorState -> { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + else -> {} + } } } @@ -284,98 +324,27 @@ class ConversationInfoEditActivity : } } - private fun uploadAvatar(file: File?) { - val builder = MultipartBody.Builder() - builder.setType(MultipartBody.FORM) - builder.addFormDataPart( - "file", - file!!.name, - file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) - ) - val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( - "file", - file.name, - file.asRequestBody(Mimetype.IMAGE_JPG.toMediaTypeOrNull()) - ) - - // upload file - ncApi.uploadConversationAvatar( - credentials, - ApiUtils.getUrlForConversationAvatar(1, conversationUser.baseUrl, conversation!!.token), - filePart - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - conversation = roomOverall.ocs!!.data - loadConversationAvatar() - } - - override fun onError(e: Throwable) { - Toast.makeText( - applicationContext, - context.getString(R.string.default_error_msg), - Toast.LENGTH_LONG - ).show() - Log.e(TAG, "Error uploading avatar", e) - } - - override fun onComplete() { - // unused atm - } - }) + private fun uploadAvatar(file: File) { + conversationInfoEditViewModel.uploadConversationAvatar(conversationUser, file, roomToken) } private fun deleteAvatar() { - ncApi.deleteConversationAvatar( - credentials, - ApiUtils.getUrlForConversationAvatar(1, conversationUser.baseUrl, conversationToken) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - conversation = roomOverall.ocs!!.data - loadConversationAvatar() - } - - override fun onError(e: Throwable) { - Toast.makeText( - applicationContext, - context.getString(R.string.default_error_msg), - Toast.LENGTH_LONG - ).show() - Log.e(TAG, "Failed to delete avatar", e) - } - - override fun onComplete() { - // unused atm - } - }) + conversationInfoEditViewModel.deleteConversationAvatar(conversationUser, roomToken) } private fun loadConversationAvatar() { setupAvatarOptions() when (conversation!!.type) { - Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) { + ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) { conversation!!.name?.let { binding.avatarImage.loadUserAvatar(conversationUser, it, true, false) } } - Conversation.ConversationType.ROOM_GROUP_CALL, Conversation.ConversationType.ROOM_PUBLIC_CALL -> { + ConversationType.ROOM_GROUP_CALL, ConversationType.ROOM_PUBLIC_CALL -> { binding.avatarImage.loadConversationAvatar(conversationUser, conversation!!, false, viewThemeUtils) } - Conversation.ConversationType.ROOM_SYSTEM -> { + ConversationType.ROOM_SYSTEM -> { binding.avatarImage.loadSystemAvatar() } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt new file mode 100644 index 000000000..99f094372 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationinfoedit.data + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import io.reactivex.Observable +import java.io.File + +interface ConversationInfoEditRepository { + + fun uploadConversationAvatar(user: User, file: File, roomToken: String): Observable + + fun deleteConversationAvatar(user: User, roomToken: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt new file mode 100644 index 000000000..0a4f49e54 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt @@ -0,0 +1,70 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationinfoedit.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File + +class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) : + ConversationInfoEditRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) + + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1)) + + override fun uploadConversationAvatar(user: User, file: File, roomToken: String): Observable { + val builder = MultipartBody.Builder() + builder.setType(MultipartBody.FORM) + builder.addFormDataPart( + "file", + file!!.name, + file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) + ) + val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_JPG.toMediaTypeOrNull()) + ) + + return ncApi.uploadConversationAvatar( + credentials, + ApiUtils.getUrlForConversationAvatar(1, user.baseUrl, roomToken), + filePart + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + } + + override fun deleteConversationAvatar(user: User, roomToken: String): Observable { + return ncApi.deleteConversationAvatar( + credentials, + ApiUtils.getUrlForConversationAvatar(1, user.baseUrl, roomToken) + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt new file mode 100644 index 000000000..31f37a2a4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt @@ -0,0 +1,141 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationinfoedit.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.io.File +import javax.inject.Inject + +class ConversationInfoEditViewModel @Inject constructor( + private val repository: ChatRepository, + private val conversationInfoEditRepository: ConversationInfoEditRepository +) : ViewModel() { + + sealed interface ViewState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + object UploadAvatarErrorState : ViewState + open class UploadAvatarSuccessState(val conversationModel: ConversationModel) : ViewState + + object DeleteAvatarErrorState : ViewState + open class DeleteAvatarSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val viewState: LiveData + get() = _viewState + + fun getRoom(user: User, token: String) { + _viewState.value = GetRoomStartState + repository.getRoom(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetRoomObserver()) + } + + fun uploadConversationAvatar(user: User, file: File, roomToken: String) { + conversationInfoEditRepository.uploadConversationAvatar(user, file, roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(UploadConversationAvatarObserver()) + } + + fun deleteConversationAvatar(user: User, roomToken: String) { + conversationInfoEditRepository.deleteConversationAvatar(user, roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(DeleteConversationAvatarObserver()) + } + + inner class GetRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = GetRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _viewState.value = GetRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class UploadConversationAvatarObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = UploadAvatarSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when uploading avatar") + _viewState.value = UploadAvatarErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class DeleteConversationAvatarObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = DeleteAvatarSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when deleting avatar") + _viewState.value = DeleteAvatarErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ConversationInfoEditViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 6ce8a7b77..ae5368d91 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -106,17 +106,14 @@ import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.ParticipantPermissions import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_CONVERSATION -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isServerEOL import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable @@ -134,7 +131,6 @@ import io.reactivex.schedulers.Schedulers import org.apache.commons.lang3.builder.CompareToBuilder import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.parceler.Parcels import retrofit2.HttpException import java.util.Objects import java.util.concurrent.TimeUnit @@ -1074,7 +1070,6 @@ class ConversationsListActivity : if (clickedItem != null) { val conversation = (clickedItem as ConversationItem).model conversationsListBottomDialog = ConversationsListBottomDialog( - this, this, userManager.currentUser.blockingGet(), conversation @@ -1185,8 +1180,6 @@ class ConversationsListActivity : } val bundle = Bundle() - bundle.putParcelable(KEY_USER_ENTITY, currentUser) - bundle.putParcelable(KEY_ACTIVE_CONVERSATION, Parcels.wrap(selectedConversation)) bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token) bundle.putString(KEY_ROOM_ID, selectedConversation!!.roomId) bundle.putString(KEY_SHARED_TEXT, textToPaste) @@ -1229,38 +1222,35 @@ class ConversationsListActivity : if (conversationMenuBundle != null && isInternalUserEqualsCurrentUser(currentUser, conversationMenuBundle) ) { - val conversation = Parcels.unwrap(conversationMenuBundle!!.getParcelable(KEY_ROOM)) - if (conversation != null) { - binding?.floatingActionButton?.let { - val dialogBuilder = MaterialAlertDialogBuilder(it.context) - .setIcon( - viewThemeUtils.dialog - .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) - ) - .setTitle(R.string.nc_delete_call) - .setMessage(R.string.nc_delete_conversation_more) - .setPositiveButton(R.string.nc_delete) { _, _ -> - val data = Data.Builder() - data.putLong( - KEY_INTERNAL_USER_ID, - conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) - ) - data.putString(KEY_ROOM_TOKEN, conversation.token) - conversationMenuBundle = null - deleteConversation(data.build()) - } - .setNegativeButton(R.string.nc_cancel) { _, _ -> - conversationMenuBundle = null - } - - viewThemeUtils.dialog - .colorMaterialAlertDialogBackground(it.context, dialogBuilder) - val dialog = dialogBuilder.show() - viewThemeUtils.platform.colorTextButtons( - dialog.getButton(AlertDialog.BUTTON_POSITIVE), - dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + binding?.floatingActionButton?.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog + .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) ) - } + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + val data = Data.Builder() + data.putLong( + KEY_INTERNAL_USER_ID, + conversationMenuBundle!!.getLong(KEY_INTERNAL_USER_ID) + ) + data.putString(KEY_ROOM_TOKEN, bundle.getString(KEY_ROOM_TOKEN)) + conversationMenuBundle = null + deleteConversation(data.build()) + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + conversationMenuBundle = null + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) } } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index b76ad0c0b..5949effb2 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -26,6 +26,10 @@ package com.nextcloud.talk.dagger.modules import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.chat.data.ChatRepository +import com.nextcloud.talk.chat.data.ChatRepositoryImpl +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl import com.nextcloud.talk.data.source.local.TalkDatabase import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl @@ -123,4 +127,16 @@ class RepositoryModule { TranslateRepository { return TranslateRepositoryImpl(ncApi) } + + @Provides + fun provideChatRepository(ncApi: NcApi): + ChatRepository { + return ChatRepositoryImpl(ncApi) + } + + @Provides + fun provideConversationInfoEditRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): + ConversationInfoEditRepository { + return ConversationInfoEditRepositoryImpl(ncApi, userProvider) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index 44a5a6f8a..f37209837 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -23,6 +23,9 @@ package com.nextcloud.talk.dagger.modules import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.nextcloud.talk.callnotification.viewmodel.CallNotificationViewModel +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel @@ -112,5 +115,20 @@ abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(OpenConversationsViewModel::class) - abstract fun openConversationsViewModelModel(viewModel: OpenConversationsViewModel): ViewModel + abstract fun openConversationsViewModel(viewModel: OpenConversationsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ChatViewModel::class) + abstract fun chatViewModel(viewModel: ChatViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(CallNotificationViewModel::class) + abstract fun callNotificationViewModel(viewModel: CallNotificationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationInfoEditViewModel::class) + abstract fun conversationInfoEditViewModel(viewModel: ConversationInfoEditViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt index 7071111bf..de64a74d3 100644 --- a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -94,6 +94,7 @@ abstract class TalkDatabase : RoomDatabase() { return Room .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) + // comment out openHelperFactory to view the database entries in Android Studio for debugging .openHelperFactory(factory) .addMigrations(Migrations.MIGRATION_6_8, Migrations.MIGRATION_7_8) .allowMainThreadQueries() diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index dae30d5ef..f2d6c439f 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -44,6 +44,8 @@ import coil.transform.RoundedCornersTransformation import com.amulyakhare.textdrawable.TextDrawable import com.nextcloud.talk.R import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils @@ -52,11 +54,26 @@ import com.nextcloud.talk.utils.DisplayUtils private const val ROUNDING_PIXEL = 16f private const val TAG = "ImageViewExtensions" +@Deprecated("use other constructor that expects com.nextcloud.talk.models.domain.ConversationModel") fun ImageView.loadConversationAvatar( user: User, conversation: Conversation, ignoreCache: Boolean, viewThemeUtils: ViewThemeUtils? +): io.reactivex.disposables.Disposable { + return loadConversationAvatar( + user, + ConversationModel.mapToConversationModel(conversation), + ignoreCache, + viewThemeUtils + ) +} + +fun ImageView.loadConversationAvatar( + user: User, + conversation: ConversationModel, + ignoreCache: Boolean, + viewThemeUtils: ViewThemeUtils? ): io.reactivex.disposables.Disposable { val imageRequestUri = ApiUtils.getUrlForConversationAvatarWithVersion( 1, @@ -68,10 +85,10 @@ fun ImageView.loadConversationAvatar( if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) { when (conversation.type) { - Conversation.ConversationType.ROOM_GROUP_CALL -> + ConversationType.ROOM_GROUP_CALL -> return loadDefaultGroupCallAvatar(viewThemeUtils) - Conversation.ConversationType.ROOM_PUBLIC_CALL -> + ConversationType.ROOM_PUBLIC_CALL -> return loadDefaultPublicCallAvatar(viewThemeUtils) else -> {} @@ -82,10 +99,10 @@ fun ImageView.loadConversationAvatar( // when no own images are set. (although these default avatars can not be themed for the android app..) val errorPlaceholder = when (conversation.type) { - Conversation.ConversationType.ROOM_GROUP_CALL -> + ConversationType.ROOM_GROUP_CALL -> ContextCompat.getDrawable(context, R.drawable.ic_circular_group) - Conversation.ConversationType.ROOM_PUBLIC_CALL -> + ConversationType.ROOM_PUBLIC_CALL -> ContextCompat.getDrawable(context, R.drawable.ic_circular_link) else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index de8f6eb68..b5d796b04 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -55,12 +55,12 @@ import autodagger.AutoInjector import com.bluelinelabs.logansquare.LoganSquare import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R -import com.nextcloud.talk.activities.CallNotificationActivity import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.callnotification.CallNotificationActivity import com.nextcloud.talk.models.SignatureVerification import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage import com.nextcloud.talk.models.json.conversations.RoomOverall @@ -94,7 +94,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder import io.reactivex.Observable @@ -197,7 +196,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor val bundle = Bundle() bundle.putString(KEY_ROOM_TOKEN, pushMessage.id) bundle.putInt(KEY_NOTIFICATION_TIMESTAMP, pushMessage.timestamp.toInt()) - bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user) + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true) fullScreenIntent.putExtras(bundle) fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK @@ -680,7 +679,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor shareRecordingIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId) shareRecordingIntent.putExtra(KEY_SHARE_RECORDING_TO_CHAT_URL, shareToChatUrl) shareRecordingIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id) - shareRecordingIntent.putExtra(KEY_USER_ENTITY, signatureVerification.user) val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT @@ -924,7 +922,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK val bundle = Bundle() bundle.putString(KEY_ROOM_TOKEN, pushMessage.id) - bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user) + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false) intent.putExtras(bundle) return intent @@ -935,7 +933,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK val bundle = Bundle() bundle.putString(KEY_ROOM_TOKEN, pushMessage.id) - bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user) + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false) intent.putExtras(bundle) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index d5f97b3ae..e6bc5370d 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -53,8 +53,8 @@ import com.nextcloud.talk.utils.FileUtils import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.RemoteFileUtils import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil import com.nextcloud.talk.utils.preferences.AppPreferences @@ -244,7 +244,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK bundle.putString(KEY_ROOM_TOKEN, roomToken) - bundle.putParcelable(KEY_USER_ENTITY, currentUser) + bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!) bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false) intent.putExtras(bundle) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt new file mode 100644 index 000000000..9859d3b8d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -0,0 +1,136 @@ +package com.nextcloud.talk.models.domain + +import com.nextcloud.talk.models.json.conversations.Conversation + +class ConversationModel( + var roomId: String?, + var token: String? = null, + var name: String? = null, + var displayName: String? = null, + var description: String? = null, + var type: ConversationType? = null, + var lastPing: Long = 0, + var participantType: ParticipantType? = null, + var hasPassword: Boolean = false, + var sessionId: String? = null, + var actorId: String? = null, + var actorType: String? = null, + var password: String? = null, + var favorite: Boolean = false, + var lastActivity: Long = 0, + var unreadMessages: Int = 0, + var unreadMention: Boolean = false, + // var lastMessage: .....? = null, + var objectType: ObjectType? = null, + var notificationLevel: NotificationLevel? = null, + var conversationReadOnlyState: ConversationReadOnlyState? = null, + var lobbyState: LobbyState? = null, + var lobbyTimer: Long? = null, + var lastReadMessage: Int = 0, + var hasCall: Boolean = false, + var callFlag: Int = 0, + var canStartCall: Boolean = false, + var canLeaveConversation: Boolean? = null, + var canDeleteConversation: Boolean? = null, + var unreadMentionDirect: Boolean? = null, + var notificationCalls: Int? = null, + var permissions: Int = 0, + var messageExpiration: Int = 0, + var status: String? = null, + var statusIcon: String? = null, + var statusMessage: String? = null, + var statusClearAt: Long? = 0, + var callRecording: Int = 0, + var avatarVersion: String? = null, + var hasCustomAvatar: Boolean? = null +) { + + companion object { + fun mapToConversationModel( + conversation: Conversation + ): ConversationModel { + return ConversationModel( + roomId = conversation.roomId, + token = conversation.token, + name = conversation.name, + displayName = conversation.displayName, + description = conversation.description, + type = conversation.type?.let { ConversationType.valueOf(it.name) }, + lastPing = conversation.lastPing, + participantType = conversation.participantType?.let { ParticipantType.valueOf(it.name) }, + hasPassword = conversation.hasPassword, + sessionId = conversation.sessionId, + actorId = conversation.actorId, + actorType = conversation.actorType, + password = conversation.password, + favorite = conversation.favorite, + lastActivity = conversation.lastActivity, + unreadMessages = conversation.unreadMessages, + unreadMention = conversation.unreadMention, + // lastMessage = conversation.lastMessage, to do... + objectType = conversation.objectType?.let { ObjectType.valueOf(it.name) }, + notificationLevel = conversation.notificationLevel?.let { + NotificationLevel.valueOf( + it.name + ) + }, + conversationReadOnlyState = conversation.conversationReadOnlyState?.let { + ConversationReadOnlyState.valueOf( + it.name + ) + }, + lobbyState = conversation.lobbyState?.let { LobbyState.valueOf(it.name) }, + lobbyTimer = conversation.lobbyTimer, + lastReadMessage = conversation.lastReadMessage, + hasCall = conversation.hasCall, + callFlag = conversation.callFlag, + canStartCall = conversation.canStartCall, + canLeaveConversation = conversation.canLeaveConversation, + canDeleteConversation = conversation.canDeleteConversation, + unreadMentionDirect = conversation.unreadMentionDirect, + notificationCalls = conversation.notificationCalls, + permissions = conversation.permissions, + messageExpiration = conversation.messageExpiration, + status = conversation.status, + statusIcon = conversation.statusIcon, + statusMessage = conversation.statusMessage, + statusClearAt = conversation.statusClearAt, + callRecording = conversation.callRecording, + avatarVersion = conversation.avatarVersion, + hasCustomAvatar = conversation.hasCustomAvatar + ) + } + } +} + +enum class ConversationType { + DUMMY, + ROOM_TYPE_ONE_TO_ONE_CALL, + ROOM_GROUP_CALL, + ROOM_PUBLIC_CALL, + ROOM_SYSTEM, + FORMER_ONE_TO_ONE +} + +enum class ParticipantType { + DUMMY, OWNER, MODERATOR, USER, GUEST, USER_FOLLOWING_LINK, GUEST_MODERATOR +} + +enum class ObjectType { + DEFAULT, + SHARE_PASSWORD, + FILE, + ROOM +} + +enum class NotificationLevel { + DEFAULT, ALWAYS, MENTION, NEVER +} + +enum class ConversationReadOnlyState { + CONVERSATION_READ_WRITE, CONVERSATION_READ_ONLY +} + +enum class LobbyState { + LOBBY_STATE_ALL_PARTICIPANTS, LOBBY_STATE_MODERATORS_ONLY +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index f2051acc4..b3a19828e 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -158,39 +158,47 @@ data class Conversation( // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null, null) + @Deprecated("Use ConversationUtil") val isPublic: Boolean get() = ConversationType.ROOM_PUBLIC_CALL == type + @Deprecated("Use ConversationUtil") val isGuest: Boolean get() = ParticipantType.GUEST == participantType || ParticipantType.GUEST_MODERATOR == participantType || ParticipantType.USER_FOLLOWING_LINK == participantType + @Deprecated("Use ConversationUtil") val isParticipantOwnerOrModerator: Boolean get() = ParticipantType.OWNER == participantType || ParticipantType.GUEST_MODERATOR == participantType || ParticipantType.MODERATOR == participantType + @Deprecated("Use ConversationUtil") private fun isLockedOneToOne(conversationUser: User): Boolean { return type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "locked-one-to-one-rooms") } + @Deprecated("Use ConversationUtil") fun canModerate(conversationUser: User): Boolean { return isParticipantOwnerOrModerator && !isLockedOneToOne(conversationUser) && type != ConversationType.FORMER_ONE_TO_ONE } + @Deprecated("Use ConversationUtil") fun isLobbyViewApplicable(conversationUser: User): Boolean { return !canModerate(conversationUser) && (type == ConversationType.ROOM_GROUP_CALL || type == ConversationType.ROOM_PUBLIC_CALL) } + @Deprecated("Use ConversationUtil") fun isNameEditable(conversationUser: User): Boolean { return canModerate(conversationUser) && ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != type } + @Deprecated("Use ConversationUtil") fun canLeave(): Boolean { return if (canLeaveConversation != null) { // Available since APIv2 @@ -200,6 +208,7 @@ data class Conversation( } } + @Deprecated("Use ConversationUtil") fun canDelete(conversationUser: User): Boolean { return if (canDeleteConversation != null) { // Available since APIv2 diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt index 5632831bf..6f1eefd83 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt @@ -79,10 +79,7 @@ class ListOpenConversationsActivity : BaseActivity() { } private fun adapterOnClick(conversation: OpenConversation) { - val user = userProvider.currentUser.blockingGet() - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, user) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.roomToken) val chatIntent = Intent(context, ChatActivity::class.java) diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt index 826c3cade..27c28dd80 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt @@ -37,6 +37,7 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogPollMainBinding import com.nextcloud.talk.polls.viewmodels.PollMainViewModel import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -48,16 +49,22 @@ class PollMainDialogFragment : DialogFragment() { @Inject lateinit var viewThemeUtils: ViewThemeUtils + var currentUserProvider: CurrentUserProviderNew? = null + @Inject set + private lateinit var binding: DialogPollMainBinding private lateinit var viewModel: PollMainViewModel + lateinit var user: User + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java] - val user: User = arguments?.getParcelable(KEY_USER_ENTITY)!! + user = currentUserProvider?.currentUser?.blockingGet()!! + val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! val isOwnerOrModerator = arguments?.getBoolean(KEY_OWNER_OR_MODERATOR)!! val pollId = arguments?.getString(KEY_POLL_ID)!! diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt index 1abe36622..dd52c63b5 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt @@ -56,8 +56,6 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() { lateinit var currentUser: User private var systemNotificationId: Int? = null private var link: String? = null - var roomToken: String? = null - var conversationOfShareTarget: User? = null init { NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) @@ -68,9 +66,6 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() { systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0) link = intent.getStringExtra(BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL) - roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN) - conversationOfShareTarget = intent.getParcelableExtra(BundleKeys.KEY_USER_ENTITY) - val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, userManager.currentUser.blockingGet().id!!) currentUser = userManager.getUserWithId(id).blockingGet() diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt index 912b8cc61..71a999bd5 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt @@ -23,19 +23,18 @@ package com.nextcloud.talk.repositories.reactions import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.conversations.Conversation import io.reactivex.Observable interface ReactionsRepository { fun addReaction( - currentConversation: Conversation, + roomToken: String, message: ChatMessage, emoji: String ): Observable fun deleteReaction( - currentConversation: Conversation, + roomToken: String, message: ChatMessage, emoji: String ): Observable diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt index 17eff5b01..2a99c3c42 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt @@ -25,7 +25,6 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericMeta import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew @@ -38,7 +37,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) override fun addReaction( - currentConversation: Conversation, + roomToken: String, message: ChatMessage, emoji: String ): Observable { @@ -46,7 +45,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur credentials, ApiUtils.getUrlForMessageReaction( currentUser.baseUrl, - currentConversation.token, + roomToken, message.id ), emoji @@ -54,7 +53,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur } override fun deleteReaction( - currentConversation: Conversation, + roomToken: String, message: ChatMessage, emoji: String ): Observable { @@ -62,7 +61,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur credentials, ApiUtils.getUrlForMessageReaction( currentUser.baseUrl, - currentConversation.token, + roomToken, message.id ), emoji diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt index d06a5f983..4a7c22b26 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt @@ -49,12 +49,15 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class SharedItemsActivity : AppCompatActivity() { + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -70,7 +73,9 @@ class SharedItemsActivity : AppCompatActivity() { val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME) - val user = intent.getParcelableExtra(KEY_USER_ENTITY)!! + + val user = currentUserProvider.currentUser.blockingGet() + val isUserConversationOwnerOrModerator = intent.getBooleanExtra(KEY_USER_IS_OWNER_OR_MODERATOR, false) binding = ActivitySharedItemsBinding.inflate(layoutInflater) diff --git a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt index 355355dc6..82be2318c 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt @@ -46,7 +46,6 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.parceler.Parcels private const val TAG = "ProfileBottomSheet" @@ -144,46 +143,13 @@ class ProfileBottomSheet(val ncApi: NcApi, val userModel: User) { override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, userModel) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) - // FIXME once APIv2+ is used only, the createRoom already returns all the data - ncApi.getRoom( - credentials, - ApiUtils.getUrlForRoom( - apiVersion, - userModel.baseUrl, - roomOverall.ocs!!.data!!.token - ) - ) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(roomOverall: RoomOverall) { - bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs!!.data) - ) - - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(bundle) - chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - context.startActivity(chatIntent) - } - - override fun onError(e: Throwable) { - Log.e(TAG, e.message, e) - } - - override fun onComplete() { - // unused atm - } - }) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) } override fun onError(e: Throwable) { diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java index e5f1cb6c1..87c9099a0 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java @@ -203,7 +203,7 @@ public class ChooseAccountDialogFragment extends DialogFragment { dismiss(); if (status != null) { - SetStatusDialogFragment setStatusDialog = SetStatusDialogFragment.newInstance(user, status); + SetStatusDialogFragment setStatusDialog = SetStatusDialogFragment.newInstance(status); setStatusDialog.show(getActivity().getSupportFragmentManager(), "fragment_set_status"); } else { Log.w(TAG, "status was null"); diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt index 1113fa0f6..41ccf6d98 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt @@ -20,7 +20,6 @@ package com.nextcloud.talk.ui.dialog -import android.app.Activity import android.os.Bundle import android.text.TextUtils import android.view.View @@ -55,16 +54,13 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPERATION_CODE -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew -import org.parceler.Parcels import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ConversationsListBottomDialog( - val activity: Activity, - val controller: ConversationsListActivity, + val activity: ConversationsListActivity, val currentUser: User, val conversation: Conversation ) : BottomSheetDialog(activity) { @@ -175,9 +171,8 @@ class ConversationsListBottomDialog( if (!TextUtils.isEmpty(conversation.token)) { val bundle = Bundle() bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!) - bundle.putParcelable(KEY_ROOM, Parcels.wrap(conversation)) - - controller.showDeleteConversationDialog(bundle) + bundle.putString(KEY_ROOM_TOKEN, conversation.token) + activity.showDeleteConversationDialog(bundle) } dismiss() @@ -198,8 +193,8 @@ class ConversationsListBottomDialog( private fun executeOperationsMenuController(operation: ConversationOperationEnum) { val bundle = Bundle() - bundle.putParcelable(KEY_ROOM, Parcels.wrap(conversation)) bundle.putSerializable(KEY_OPERATION_CODE, operation) + bundle.putString(KEY_ROOM_TOKEN, conversation.token) binding.operationItemsLayout.visibility = View.GONE @@ -211,13 +206,13 @@ class ConversationsListBottomDialog( .popChangeHandler(HorizontalChangeHandler()) ) - controller.fetchRooms() + activity.fetchRooms() } private fun executeEntryMenuController(operation: ConversationOperationEnum) { val bundle = Bundle() - bundle.putParcelable(KEY_ROOM, Parcels.wrap(conversation)) bundle.putSerializable(KEY_OPERATION_CODE, operation) + bundle.putString(KEY_ROOM_TOKEN, conversation.token) binding.operationItemsLayout.visibility = View.GONE diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index 565bb8c76..0ea98c59a 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -40,10 +40,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogMessageActionsBinding +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationReadOnlyState +import com.nextcloud.talk.models.domain.ConversationType import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew @@ -63,7 +65,7 @@ class MessageActionsDialog( private val chatActivity: ChatActivity, private val message: ChatMessage, private val user: User?, - private val currentConversation: Conversation?, + private val currentConversation: ConversationModel?, private val showMessageDeletionButton: Boolean, private val hasChatPermission: Boolean ) : BottomSheetDialog(chatActivity) { @@ -100,7 +102,7 @@ class MessageActionsDialog( message.replyable && hasUserId(user) && hasUserActorId(message) && - currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + currentConversation?.type != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ) initMenuDeleteMessage(showMessageDeletionButton) initMenuForwardMessage( @@ -226,7 +228,7 @@ class MessageActionsDialog( } private fun isPermitted(hasChatPermission: Boolean): Boolean { - return hasChatPermission && Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY != + return hasChatPermission && ConversationReadOnlyState.CONVERSATION_READ_ONLY != currentConversation?.conversationReadOnlyState } @@ -338,12 +340,12 @@ class MessageActionsDialog( private fun clickOnEmoji(message: ChatMessage, emoji: String) { if (message.reactionsSelf?.contains(emoji) == true) { - reactionsRepository.deleteReaction(currentConversation!!, message, emoji) + reactionsRepository.deleteReaction(currentConversation!!.token!!, message, emoji) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(ReactionDeletedObserver()) } else { - reactionsRepository.addReaction(currentConversation!!, message, emoji) + reactionsRepository.addReaction(currentConversation!!.token!!, message, emoji) .subscribeOn(Schedulers.io()) ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(ReactionAddedObserver()) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt index 071bfb2b9..354440bdc 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt @@ -60,6 +60,7 @@ import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.installDisableKeyboardInput import com.vanniktech.emoji.installForceSingleEmoji @@ -113,6 +114,9 @@ class SetStatusDialogFragment : @Inject lateinit var viewThemeUtils: ViewThemeUtils + var currentUserProvider: CurrentUserProviderNew? = null + @Inject set + lateinit var credentials: String override fun onCreate(savedInstanceState: Bundle?) { @@ -121,7 +125,7 @@ class SetStatusDialogFragment : NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) arguments?.let { - currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM) + currentUser = currentUserProvider?.currentUser?.blockingGet() currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM) credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token) @@ -559,9 +563,8 @@ class SetStatusDialogFragment : private val TAG = SetStatusDialogFragment::class.simpleName @JvmStatic - fun newInstance(user: User, status: Status): SetStatusDialogFragment { + fun newInstance(status: Status): SetStatusDialogFragment { val args = Bundle() - args.putParcelable(ARG_CURRENT_USER_PARAM, user) args.putParcelable(ARG_CURRENT_STATUS_PARAM, status) val dialogFragment = SetStatusDialogFragment() diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt index 0bba68df4..196a21b2c 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt @@ -44,7 +44,6 @@ import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.DialogMessageReactionsBinding import com.nextcloud.talk.databinding.ItemReactionsTabBinding import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.reactions.ReactionsOverall import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -59,7 +58,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ShowReactionsDialog( activity: Activity, - private val currentConversation: Conversation?, + private val roomToken: String, private val chatMessage: ChatMessage, private val user: User?, private val hasChatPermission: Boolean, @@ -156,7 +155,7 @@ class ShowReactionsDialog( credentials, ApiUtils.getUrlForMessageReaction( user?.baseUrl, - currentConversation!!.token, + roomToken, chatMessage.id ), emoji @@ -211,7 +210,7 @@ class ShowReactionsDialog( credentials, ApiUtils.getUrlForMessageReaction( user?.baseUrl, - currentConversation!!.token, + roomToken, message.id ), emoji diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt new file mode 100644 index 000000000..cc125ff0a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt @@ -0,0 +1,89 @@ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ConversationType +import com.nextcloud.talk.models.domain.ParticipantType +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew + +/* + * Nextcloud Talk application + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * 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 . + */ + +object ConversationUtils { + private val TAG = ConversationUtils::class.java.simpleName + + fun isPublic(conversation: ConversationModel): Boolean { + return ConversationType.ROOM_PUBLIC_CALL == conversation.type + } + + fun isGuest(conversation: ConversationModel): Boolean { + return ParticipantType.GUEST == conversation.participantType || + ParticipantType.GUEST_MODERATOR == conversation.participantType || + ParticipantType.USER_FOLLOWING_LINK == conversation.participantType + } + + fun isParticipantOwnerOrModerator(conversation: ConversationModel): Boolean { + return ParticipantType.OWNER == conversation.participantType || + ParticipantType.GUEST_MODERATOR == conversation.participantType || + ParticipantType.MODERATOR == conversation.participantType + } + + private fun isLockedOneToOne(conversation: ConversationModel, conversationUser: User): Boolean { + return conversation.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "locked-one-to-one-rooms") + } + + fun canModerate(conversation: ConversationModel, conversationUser: User): Boolean { + return isParticipantOwnerOrModerator(conversation) && + !isLockedOneToOne(conversation, conversationUser) && + conversation.type != ConversationType.FORMER_ONE_TO_ONE + } + + fun isLobbyViewApplicable(conversation: ConversationModel, conversationUser: User): Boolean { + return !canModerate(conversation, conversationUser) && + ( + conversation.type == ConversationType.ROOM_GROUP_CALL || + conversation.type == ConversationType.ROOM_PUBLIC_CALL + ) + } + + fun isNameEditable(conversation: ConversationModel, conversationUser: User): Boolean { + return canModerate(conversation, conversationUser) && + ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation.type + } + + fun canLeave(conversation: ConversationModel): Boolean { + return if (conversation.canLeaveConversation != null) { + // Available since APIv2 + conversation.canLeaveConversation!! + } else { + true + } + } + + fun canDelete(conversation: ConversationModel, conversationUser: User): Boolean { + return if (conversation.canDeleteConversation != null) { + // Available since APIv2 + conversation.canDeleteConversation!! + } else { + canModerate(conversation, conversationUser) + // Fallback for APIv1 + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt index cadb8d894..dddb8753e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt @@ -23,6 +23,7 @@ package com.nextcloud.talk.utils import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew @@ -31,9 +32,15 @@ import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew */ class ParticipantPermissions( private val user: User, - private val conversation: Conversation + private val conversation: ConversationModel ) { + @Deprecated("Use ChatRepository.ConversationModel") + constructor(user: User, conversation: Conversation) : this( + user, + ConversationModel.mapToConversationModel(conversation) + ) + val isDefault = (conversation.permissions and DEFAULT) == DEFAULT val isCustom = (conversation.permissions and CUSTOM) == CUSTOM private val canStartCall = (conversation.permissions and START_CALL) == START_CALL diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index cacd7c17e..d5b9cae6e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -33,19 +33,17 @@ object BundleKeys { const val KEY_BASE_URL = "KEY_BASE_URL" const val KEY_IS_ACCOUNT_IMPORT = "KEY_IS_ACCOUNT_IMPORT" const val KEY_ORIGINAL_PROTOCOL = "KEY_ORIGINAL_PROTOCOL" - const val KEY_ROOM = "KEY_CONVERSATION" const val KEY_OPERATION_CODE = "KEY_OPERATION_CODE" - const val KEY_SHARE_INTENT = "KEY_SHARE_INTENT" const val KEY_APP_ITEM_PACKAGE_NAME = "KEY_APP_ITEM_PACKAGE_NAME" const val KEY_APP_ITEM_NAME = "KEY_APP_ITEM_NAME" const val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD" const val KEY_ROOM_TOKEN = "KEY_ROOM_TOKEN" const val KEY_ROOM_ONE_TO_ONE = "KEY_ROOM_ONE_TO_ONE" - const val KEY_USER_ENTITY = "KEY_USER_ENTITY" const val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION" const val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS" const val KEY_EXISTING_PARTICIPANTS = "KEY_EXISTING_PARTICIPANTS" const val KEY_CALL_URL = "KEY_CALL_URL" + const val KEY_NEW_ROOM_NAME = "KEY_NEW_ROOM_NAME" const val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL" const val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT" const val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE" @@ -59,8 +57,6 @@ object BundleKeys { const val KEY_RECORDING_STATE = "KEY_RECORDING_STATE" const val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY" const val KEY_CALL_WITHOUT_NOTIFICATION = "KEY_CALL_WITHOUT_NOTIFICATION" - const val KEY_ACTIVE_CONVERSATION = "KEY_ACTIVE_CONVERSATION" - const val KEY_SERVER_CAPABILITIES = "KEY_SERVER_CAPABILITIES" const val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL" const val KEY_ROOM_ID = "KEY_ROOM_ID" const val KEY_ARE_CALL_SOUNDS = "KEY_ARE_CALL_SOUNDS"