Browse Source

pass spreedCapabilities instead user to CapabilitiesUtil

To support federated rooms, capabilities have to be checked from the room which now also has capabilities.
If room is not federated, capabilities fromuser are still checked.
This is why CapabilitiesUtil had to be refactored to accept SpreedCapabilities which can come from room or user.

Other than that, many other changes were made as a result of this change.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 year ago
parent
commit
754b825096
100 changed files with 2084 additions and 1554 deletions
  1. 3 3
      app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt
  2. 2 2
      app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt
  3. 37 28
      app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
  4. 3 3
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  5. 3 2
      app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt
  6. 2 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt
  7. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt
  8. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  9. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt
  10. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt
  11. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  12. 2 2
      app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt
  13. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt
  14. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  15. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt
  16. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt
  17. 1 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
  18. 5 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  19. 9 8
      app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt
  20. 174 124
      app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  21. 11 3
      app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt
  22. 42 18
      app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt
  23. 77 38
      app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
  24. 1 1
      app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFolderListingOperation.kt
  25. 23 10
      app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt
  26. 6 6
      app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt
  27. 196 165
      app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt
  28. 8 4
      app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt
  29. 142 0
      app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt
  30. 13 8
      app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt
  31. 4 4
      app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt
  32. 30 15
      app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
  33. 6 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  34. 3 3
      app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt
  35. 1 1
      app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
  36. 4 4
      app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt
  37. 6 6
      app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt
  38. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java
  39. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
  40. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java
  41. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.java
  42. 14 14
      app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
  43. 2 2
      app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt
  44. 1 1
      app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java
  45. 4 2
      app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  46. 5 0
      app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt
  47. 5 2
      app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt
  48. 1 1
      app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt
  49. 11 3
      app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt
  50. 45 0
      app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt
  51. 42 0
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt
  52. 37 0
      app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt
  53. 7 7
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt
  54. 18 4
      app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt
  55. 3 3
      app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt
  56. 5 5
      app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt
  57. 4 4
      app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java
  58. 16 11
      app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt
  59. 1 1
      app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt
  60. 3 3
      app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt
  61. 3 3
      app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt
  62. 1 1
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt
  63. 3 3
      app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt
  64. 3 3
      app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt
  65. 3 3
      app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt
  66. 1 1
      app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt
  67. 20 15
      app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt
  68. 3 3
      app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt
  69. 4 4
      app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt
  70. 3 3
      app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt
  71. 7 6
      app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  72. 2 2
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  73. 1 1
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt
  74. 25 14
      app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt
  75. 14 6
      app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt
  76. 16 9
      app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
  77. 3 3
      app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt
  78. 6 6
      app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt
  79. 2 2
      app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt
  80. 4 4
      app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt
  81. 1 1
      app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt
  82. 1 1
      app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt
  83. 0 559
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  84. 577 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt
  85. 275 0
      app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt
  86. 11 12
      app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt
  87. 1 2
      app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt
  88. 7 8
      app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt
  89. 1 1
      app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt
  90. 2 2
      app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt
  91. 2 2
      app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt
  92. 1 0
      app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
  93. 0 267
      app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt
  94. 4 3
      app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java
  95. 3 4
      app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt
  96. 1 1
      app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java
  97. 3 3
      app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt
  98. 4 4
      app/src/test/java/com/nextcloud/talk/utils/ShareUtilsTest.kt
  99. 1 1
      detekt.yml
  100. 14 54
      gradle/verification-keyring.keys

+ 3 - 3
app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt

@@ -199,7 +199,7 @@ class AccountVerificationActivity : BaseActivity() {
         val credentials = ApiUtils.getCredentials(username, token)
         cookieManager.cookieStore.removeAll()
 
-        ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
+        ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!))
             .subscribeOn(Schedulers.io())
             .subscribe(object : Observer<CapabilitiesOverall> {
                 override fun onSubscribe(d: Disposable) {
@@ -213,7 +213,7 @@ class AccountVerificationActivity : BaseActivity() {
                             capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features != null &&
                             !capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features!!.isEmpty()
                     if (hasTalk) {
-                        fetchProfile(credentials, capabilitiesOverall)
+                        fetchProfile(credentials!!, capabilitiesOverall)
                     } else {
                         if (resources != null) {
                             runOnUiThread {
@@ -305,7 +305,7 @@ class AccountVerificationActivity : BaseActivity() {
     private fun fetchProfile(credentials: String, capabilitiesOverall: CapabilitiesOverall) {
         ncApi.getUserProfile(
             credentials,
-            ApiUtils.getUrlForUserProfile(baseUrl)
+            ApiUtils.getUrlForUserProfile(baseUrl!!)
         )
             .subscribeOn(Schedulers.io())
             .subscribe(object : Observer<UserProfileOverall> {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt

@@ -52,7 +52,7 @@ import com.nextcloud.talk.utils.UriUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
@@ -336,7 +336,7 @@ class ServerSelectionActivity : BaseActivity() {
 
                     if (hasTalk) {
                         runOnUiThread {
-                            if (CapabilitiesUtilNew.isServerEOL(capabilities)) {
+                            if (CapabilitiesUtil.isServerEOL(capabilitiesOverall.ocs?.data?.serverVersion?.major!!)) {
                                 if (resources != null) {
                                     runOnUiThread {
                                         setErrorText(resources!!.getString(R.string.nc_settings_server_eol))

+ 37 - 28
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

@@ -110,6 +110,7 @@ import com.nextcloud.talk.ui.dialog.AudioOutputDialog
 import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom
 import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
@@ -131,9 +132,9 @@ 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.database.user.CapabilitiesUtilNew
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isCallRecordingAvailable
+import com.nextcloud.talk.utils.CapabilitiesUtil
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
+import com.nextcloud.talk.utils.CapabilitiesUtil.isCallRecordingAvailable
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.power.PowerManagerUtils
@@ -234,7 +235,7 @@ class CallActivity : CallBaseActivity() {
     private var iceServers: MutableList<PeerConnection.IceServer>? = null
     private var cameraEnumerator: CameraEnumerator? = null
     private var roomToken: String? = null
-    var conversationUser: User? = null
+    lateinit var conversationUser: User
     private var conversationName: String? = null
     private var callSession: String? = null
     private var localStream: MediaStream? = null
@@ -530,13 +531,13 @@ class CallActivity : CallBaseActivity() {
             )
         }
 
-        when (CapabilitiesUtilNew.getRecordingConsentType(conversationUser)) {
-            CapabilitiesUtilNew.RECORDING_CONSENT_NOT_REQUIRED -> initiateCall()
-            CapabilitiesUtilNew.RECORDING_CONSENT_REQUIRED -> askForRecordingConsent()
-            CapabilitiesUtilNew.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> {
+        when (CapabilitiesUtil.getRecordingConsentType(conversationUser!!.capabilities!!.spreedCapability!!)) {
+            CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED -> initiateCall()
+            CapabilitiesUtil.RECORDING_CONSENT_REQUIRED -> askForRecordingConsent()
+            CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> {
                 val getRoomApiVersion = ApiUtils.getConversationApiVersion(
-                    conversationUser,
-                    intArrayOf(ApiUtils.APIv4, 1)
+                    conversationUser!!,
+                    intArrayOf(ApiUtils.API_V4, 1)
                 )
                 ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(getRoomApiVersion, baseUrl, roomToken))
                     .retry(API_RETRIES)
@@ -571,7 +572,10 @@ class CallActivity : CallBaseActivity() {
 
     override fun onResume() {
         super.onResume()
-        if (hasSpreedFeatureCapability(conversationUser, "recording-v1") &&
+        if (hasSpreedFeatureCapability(
+                conversationUser.capabilities!!.spreedCapability!!,
+                SpreedFeatures.RECORDING_V1
+            ) &&
             othersInCall &&
             elapsedSeconds.toInt() >= CALL_TIME_ONE_HOUR
         ) {
@@ -1468,7 +1472,7 @@ class CallActivity : CallBaseActivity() {
 
     private fun fetchSignalingSettings() {
         Log.d(TAG, "fetchSignalingSettings")
-        val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+        val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1))
         ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl))
             .subscribeOn(Schedulers.io())
             .retry(API_RETRIES)
@@ -1531,7 +1535,7 @@ class CallActivity : CallBaseActivity() {
     private fun addIceServers(signalingSettingsOverall: SignalingSettingsOverall, apiVersion: Int) {
         if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) {
             val stunServers = signalingSettingsOverall.ocs!!.settings!!.stunServers
-            if (apiVersion == ApiUtils.APIv3) {
+            if (apiVersion == ApiUtils.API_V3) {
                 for ((_, urls) in stunServers!!) {
                     if (urls != null) {
                         for (url in urls) {
@@ -1564,7 +1568,7 @@ class CallActivity : CallBaseActivity() {
     }
 
     private fun checkCapabilities() {
-        ncApi!!.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
+        ncApi!!.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!))
             .retry(API_RETRIES)
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
@@ -1600,7 +1604,7 @@ class CallActivity : CallBaseActivity() {
 
     private fun joinRoomAndCall() {
         callSession = ApplicationWideCurrentRoomHolder.getInstance().session
-        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
         Log.d(TAG, "joinRoomAndCall")
         Log.d(TAG, "   baseUrl= $baseUrl")
         Log.d(TAG, "   roomToken= $roomToken")
@@ -1656,7 +1660,7 @@ class CallActivity : CallBaseActivity() {
         fun getRoomAndContinue() {
             val getRoomApiVersion = ApiUtils.getConversationApiVersion(
                 conversationUser,
-                intArrayOf(ApiUtils.APIv4, 1)
+                intArrayOf(ApiUtils.API_V4, 1)
             )
             ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(getRoomApiVersion, baseUrl, roomToken))
                 .retry(API_RETRIES)
@@ -1715,10 +1719,10 @@ class CallActivity : CallBaseActivity() {
         callParticipantList = CallParticipantList(signalingMessageReceiver)
         callParticipantList!!.addObserver(callParticipantListObserver)
 
-        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
         ncApi!!.joinCall(
             credentials,
-            ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken),
+            ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken!!),
             inCallFlag,
             isCallWithoutNotification,
             recordingConsentGiven
@@ -1756,7 +1760,10 @@ class CallActivity : CallBaseActivity() {
     }
 
     private fun startCallTimeCounter(callStartTime: Long?) {
-        if (callStartTime != null && hasSpreedFeatureCapability(conversationUser, "recording-v1")) {
+        if (callStartTime != null && hasSpreedFeatureCapability(
+                conversationUser!!.capabilities!!.spreedCapability!!, SpreedFeatures.RECORDING_V1
+            )
+        ) {
             binding!!.callDuration.visibility = View.VISIBLE
             val currentTimeInSec = System.currentTimeMillis() / SECOND_IN_MILLIES
             elapsedSeconds = currentTimeInSec - callStartTime
@@ -1793,7 +1800,7 @@ class CallActivity : CallBaseActivity() {
     }
 
     private fun pullSignalingMessages() {
-        val signalingApiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+        val signalingApiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1))
         val delayOnError = AtomicInteger(0)
 
         ncApi!!.pullSignalingMessages(
@@ -1801,7 +1808,7 @@ class CallActivity : CallBaseActivity() {
             ApiUtils.getUrlForSignaling(
                 signalingApiVersion,
                 baseUrl,
-                roomToken
+                roomToken!!
             )
         )
             .subscribeOn(Schedulers.io())
@@ -2031,12 +2038,12 @@ class CallActivity : CallBaseActivity() {
 
     private fun hangupNetworkCalls(shutDownView: Boolean) {
         Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView")
-        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
         if (callParticipantList != null) {
             callParticipantList!!.removeObserver(callParticipantListObserver)
             callParticipantList!!.destroy()
         }
-        ncApi!!.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
+        ncApi!!.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken!!))
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<GenericOverall> {
@@ -2919,10 +2926,10 @@ class CallActivity : CallBaseActivity() {
             val strings: MutableList<String> = ArrayList()
             val stringToSend = stringBuilder.toString()
             strings.add(stringToSend)
-            val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+            val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1))
             ncApi!!.sendSignalingMessages(
                 credentials,
-                ApiUtils.getUrlForSignaling(apiVersion, baseUrl, roomToken),
+                ApiUtils.getUrlForSignaling(apiVersion, baseUrl, roomToken!!),
                 strings.toString()
             )
                 .retry(API_RETRIES)
@@ -3099,12 +3106,14 @@ class CallActivity : CallBaseActivity() {
 
     val isAllowedToStartOrStopRecording: Boolean
         get() = (
-            isCallRecordingAvailable(conversationUser!!) &&
+            isCallRecordingAvailable(conversationUser!!.capabilities!!.spreedCapability!!) &&
                 isModerator
             )
     val isAllowedToRaiseHand: Boolean
-        get() = hasSpreedFeatureCapability(conversationUser, "raise-hand") ||
-            isBreakoutRoom
+        get() = hasSpreedFeatureCapability(
+            conversationUser.capabilities!!.spreedCapability!!,
+            SpreedFeatures.RAISE_HAND
+        ) || isBreakoutRoom
 
     private inner class SelfVideoTouchListener : OnTouchListener {
         @SuppressLint("ClickableViewAccessibility")

+ 3 - 3
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -181,7 +181,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
                     val user = userId.substringBeforeLast("@")
                     val baseUrl = userId.substringAfterLast("@")
 
-                    if (userManager.currentUser.blockingGet()?.baseUrl?.endsWith(baseUrl) == true) {
+                    if (userManager.currentUser.blockingGet()?.baseUrl!!.endsWith(baseUrl) == true) {
                         startConversation(user)
                     } else {
                         Snackbar.make(
@@ -200,11 +200,11 @@ class MainActivity : BaseActivity(), ActionBarProvider {
 
         val currentUser = userManager.currentUser.blockingGet()
 
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1))
         val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
         val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
             apiVersion,
-            currentUser?.baseUrl,
+            currentUser?.baseUrl!!,
             roomType,
             null,
             userId,

+ 3 - 2
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt

@@ -50,9 +50,10 @@ import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
 import com.nextcloud.talk.ui.StatusDrawable
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.DisplayUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
 import eu.davidea.flexibleadapter.items.IFilterable
@@ -312,7 +313,7 @@ class ConversationItem(
         if (model.type === ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
             viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
         } else if (model.unreadMention) {
-            if (hasSpreedFeatureCapability(user, "direct-mention-flag")) {
+            if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) {
                 if (model.unreadMentionDirect!!) {
                     viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble)
                 } else {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedViewHolder.kt

@@ -77,12 +77,12 @@ class CallStartedViewHolder(incomingView: View, payload: Any) :
         val user = userManager.currentUser.blockingGet()
         val url: String = if (message.actorType == "guests" || message.actorType == "guest") {
             ApiUtils.getUrlForGuestAvatar(
-                user!!.baseUrl,
+                user!!.baseUrl!!,
                 message.actorDisplayName,
                 true
             )
         } else {
-            ApiUtils.getUrlForAvatar(user!!.baseUrl, message.actorDisplayName, false)
+            ApiUtils.getUrlForAvatar(user!!.baseUrl!!, message.actorDisplayName, false)
         }
 
         val imageRequest: ImageRequest = ImageRequest.Builder(context)

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt

@@ -188,7 +188,7 @@ class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -172,7 +172,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt

@@ -195,7 +195,7 @@ class IncomingPollMessageViewHolder(incomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt

@@ -197,7 +197,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
             binding.messageQuote.quotedMessageImage.load(it) {
                 addHeader(
                     "Authorization",
-                    ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                 )
             }
         } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -301,7 +301,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt

@@ -45,8 +45,8 @@ class LinkPreview {
         binding.referenceThumbImage.setImageDrawable(null)
 
         if (!message.extractedUrlToPreview.isNullOrEmpty()) {
-            val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)
-            val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl)
+            val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)!!
+            val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl!!)
             ncApi.getOpenGraph(
                 credentials,
                 openGraphLink,

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt

@@ -161,7 +161,7 @@ class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt

@@ -213,7 +213,7 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt

@@ -175,7 +175,7 @@ class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt

@@ -170,7 +170,7 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
             binding.messageQuote.quotedMessageImage.load(it) {
                 addHeader(
                     "Authorization",
-                    ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                    ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                 )
             }
         } ?: run {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -285,7 +285,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) :
                 binding.messageQuote.quotedMessageImage.load(it) {
                     addHeader(
                         "Authorization",
-                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)
+                        ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!!
                     )
                 }
             } ?: run {

+ 5 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -24,6 +24,7 @@
 package com.nextcloud.talk.api;
 
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
+import com.nextcloud.talk.models.json.capabilities.RoomCapabilitiesOverall;
 import com.nextcloud.talk.models.json.chat.ChatOverall;
 import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage;
 import com.nextcloud.talk.models.json.chat.ChatShareOverall;
@@ -367,6 +368,10 @@ public interface NcApi {
     @GET
     Observable<CapabilitiesOverall> getCapabilities(@Url String url);
 
+    @GET
+    Observable<RoomCapabilitiesOverall> getRoomCapabilities(@Header("Authorization") String authorization,
+                                                            @Url String url);
+
     /*
        QueryMap items are as follows:
          - "lookIntoFuture": int (0 or 1),

+ 9 - 8
app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt

@@ -50,6 +50,7 @@ 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.SpreedFeatures
 import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.NotificationUtils
 import com.nextcloud.talk.utils.ParticipantPermissions
@@ -57,7 +58,7 @@ 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_TOKEN
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
 import io.reactivex.disposables.Disposable
 import okhttp3.Cache
 import java.io.IOException
@@ -148,10 +149,10 @@ class CallNotificationActivity : CallBaseActivity() {
 
     private fun initObservers() {
         val apiVersion = ApiUtils.getConversationApiVersion(
-            userBeingCalled,
+            userBeingCalled!!,
             intArrayOf(
-                ApiUtils.APIv4,
-                ApiUtils.APIv3,
+                ApiUtils.API_V4,
+                ApiUtils.API_V3,
                 1
             )
         )
@@ -186,10 +187,10 @@ class CallNotificationActivity : CallBaseActivity() {
 
                     showAnswerControls()
 
-                    if (apiVersion >= ApiUtils.APIv3) {
+                    if (apiVersion >= ApiUtils.API_V3) {
                         val hasCallFlags = hasSpreedFeatureCapability(
-                            userBeingCalled,
-                            "conversation-call-flags"
+                            userBeingCalled?.capabilities?.spreedCapability!!,
+                            SpreedFeatures.CONVERSATION_CALL_FLAGS
                         )
                         if (hasCallFlags) {
                             if (isInCallWithVideo(currentConversation!!.callFlag)) {
@@ -243,7 +244,7 @@ class CallNotificationActivity : CallBaseActivity() {
             originalBundle!!.putString(KEY_CONVERSATION_NAME, currentConversation!!.displayName)
 
             val participantPermission = ParticipantPermissions(
-                userBeingCalled!!,
+                userBeingCalled!!.capabilities!!.spreedCapability!!,
                 currentConversation!!
             )
             originalBundle!!.putBoolean(

+ 174 - 124
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -167,6 +167,7 @@ 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.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatOverall
 import com.nextcloud.talk.models.json.chat.ReadStatus
@@ -192,6 +193,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.AudioUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ContactUtils
 import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.DateConstants
@@ -218,7 +220,7 @@ 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.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.rx.DisposableSet
@@ -301,6 +303,8 @@ class ChatActivity :
     var sessionIdAfterRoomJoined: String? = null
     lateinit var roomToken: String
     var conversationUser: User? = null
+    lateinit var spreedCapabilities: SpreedCapability
+    var chatApiVersion: Int = 1
     private var roomPassword: String = ""
     var credentials: String? = null
     var currentConversation: ConversationModel? = null
@@ -351,6 +355,7 @@ class ChatActivity :
         RELEASED,
         ERROR
     }
+
     private val editableBehaviorSubject = BehaviorSubject.createDefault(false)
     private val editedTextBehaviorSubject = BehaviorSubject.createDefault("")
 
@@ -541,14 +546,6 @@ class ChatActivity :
         }
         this.lifecycle.addObserver(AudioUtils)
         this.lifecycle.addObserver(ChatViewModel.LifeCycleObserver)
-
-        chatViewModel.refreshChatParams(
-            setupFieldsForPullChatMessages(
-                false,
-                0,
-                false
-            )
-        )
     }
 
     override fun onStop() {
@@ -587,6 +584,30 @@ class ChatActivity :
                 is ChatViewModel.GetRoomSuccessState -> {
                     currentConversation = state.conversationModel
                     logConversationInfos("GetRoomSuccessState")
+                    chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
+                }
+
+                is ChatViewModel.GetRoomErrorState -> {
+                    Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
+                }
+
+                else -> {}
+            }
+        }
+
+        chatViewModel.getCapabilitiesViewState.observe(this) { state ->
+            when (state) {
+                is ChatViewModel.GetCapabilitiesSuccessState -> {
+                    spreedCapabilities = state.spreedCapabilities
+                    chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
+
+                    initMessageInputView()
+
+                    if (conversationUser?.userId != "?" &&
+                        CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
+                    ) {
+                        binding.chatToolbar.setOnClickListener { v -> showConversationInfoScreen() }
+                    }
 
                     if (adapter == null) {
                         initAdapter()
@@ -597,7 +618,7 @@ class ChatActivity :
 
                     loadAvatarForStatusBar()
                     setActionBarTitle()
-                    participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
+                    participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
 
                     setupSwipeToReply()
                     setupMentionAutocomplete()
@@ -626,9 +647,17 @@ class ChatActivity :
                         },
                         delayForRecursiveCall
                     )
+
+                    chatViewModel.refreshChatParams(
+                        setupFieldsForPullChatMessages(
+                            false,
+                            0,
+                            false
+                        )
+                    )
                 }
 
-                is ChatViewModel.GetRoomErrorState -> {
+                is ChatViewModel.GetCapabilitiesErrorState -> {
                     Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
                 }
 
@@ -716,6 +745,7 @@ class ChatActivity :
                     }
                     binding.messagesListView.smoothScrollToPosition(0)
                 }
+
                 is ChatViewModel.SendChatMessageErrorState -> {
                     if (state.e is HttpException) {
                         val code = state.e.code()
@@ -730,6 +760,7 @@ class ChatActivity :
                         }
                     }
                 }
+
                 else -> {}
             }
         }
@@ -753,9 +784,11 @@ class ChatActivity :
                         )
                     )
                 }
+
                 is ChatViewModel.DeleteChatMessageErrorState -> {
                     Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
                 }
+
                 else -> {}
             }
         }
@@ -774,24 +807,22 @@ class ChatActivity :
                         startActivity(chatIntent)
                     }
                 }
+
                 is ChatViewModel.CreateRoomErrorState -> {
                     Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
                 }
+
                 else -> {}
             }
         }
 
-        var apiVersion = 1
-        // FIXME this is a best guess, guests would need to get the capabilities themselves
-        if (conversationUser != null) {
-            apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
-        }
-
-        chatViewModel.getFieldMapForChat.observe(this) { _ ->
-            chatViewModel.pullChatMessages(
-                credentials!!,
-                ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken)
-            )
+        chatViewModel.getFieldMapForChat.observe(this) { fieldMap ->
+            if (fieldMap.isNotEmpty()) {
+                chatViewModel.pullChatMessages(
+                    credentials!!,
+                    ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
+                )
+            }
         }
 
         chatViewModel.pullChatMessageViewState.observe(this) { state ->
@@ -865,6 +896,7 @@ class ChatActivity :
                                 )
                             )
                         }
+
                         HTTP_CODE_NOT_MODIFIED -> {
                             processHeaderChatLastGiven(state.response, state.lookIntoFuture)
                             chatViewModel.refreshChatParams(
@@ -875,6 +907,7 @@ class ChatActivity :
                                 )
                             )
                         }
+
                         HTTP_CODE_PRECONDITION_FAILED -> {
                             processHeaderChatLastGiven(state.response, state.lookIntoFuture)
                             chatViewModel.refreshChatParams(
@@ -885,6 +918,7 @@ class ChatActivity :
                                 )
                             )
                         }
+
                         else -> {}
                     }
 
@@ -898,12 +932,15 @@ class ChatActivity :
                         collapseSystemMessages()
                     }
                 }
+
                 is ChatViewModel.PullChatMessageCompleteState -> {
                     Log.d(TAG, "PullChatMessageCompleted")
                 }
+
                 is ChatViewModel.PullChatMessageErrorState -> {
                     Log.d(TAG, "PullChatMessageError")
                 }
+
                 else -> {}
             }
         }
@@ -916,6 +953,7 @@ class ChatActivity :
                         state.reactionDeletedModel.emoji
                     )
                 }
+
                 else -> {}
             }
         }
@@ -928,6 +966,7 @@ class ChatActivity :
                         state.reactionAddedModel.emoji
                     )
                 }
+
                 else -> {}
             }
         }
@@ -943,6 +982,7 @@ class ChatActivity :
                                 Snackbar.LENGTH_LONG
                             ).show()
                         }
+
                         HTTP_FORBIDDEN -> {
                             Snackbar.make(
                                 binding.root,
@@ -950,6 +990,7 @@ class ChatActivity :
                                 Snackbar.LENGTH_LONG
                             ).show()
                         }
+
                         HTTP_NOT_FOUND -> {
                             Snackbar.make(
                                 binding.root,
@@ -960,9 +1001,11 @@ class ChatActivity :
                     }
                     clearEditUI()
                 }
+
                 is ChatViewModel.EditMessageErrorState -> {
                     Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
                 }
+
                 else -> {}
             }
         }
@@ -980,12 +1023,6 @@ class ChatActivity :
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
 
-        if (conversationUser?.userId != "?" &&
-            CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag")
-        ) {
-            binding.chatToolbar.setOnClickListener { v -> showConversationInfoScreen() }
-        }
-
         initSmileyKeyboardToggler()
 
         themeMessageInputView()
@@ -1053,7 +1090,6 @@ class ChatActivity :
             }
         })
 
-        initMessageInputView()
         loadAvatarForStatusBar()
         setActionBarTitle()
         viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
@@ -1061,7 +1097,7 @@ class ChatActivity :
 
     private fun initMessageInputView() {
         val filters = arrayOfNulls<InputFilter>(1)
-        val lengthFilter = CapabilitiesUtilNew.getMessageMaxLength(conversationUser)
+        val lengthFilter = CapabilitiesUtil.getMessageMaxLength(spreedCapabilities)
 
         binding.editView.editMessageView.visibility = View.GONE
 
@@ -1160,7 +1196,7 @@ class ChatActivity :
             clearEditUI()
         }
 
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-send")) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_SEND)) {
             binding.messageInputView.button?.setOnLongClickListener {
                 showSendButtonMenu()
                 true
@@ -1175,14 +1211,14 @@ class ChatActivity :
         var apiVersion = 1
         // FIXME Fix API checking with guests?
         if (conversationUser != null) {
-            apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+            apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
         }
 
         chatViewModel.editChatMessage(
             credentials!!,
             ApiUtils.getUrlForChatMessage(
                 apiVersion,
-                conversationUser?.baseUrl,
+                conversationUser?.baseUrl!!,
                 roomToken,
                 message.id
             ),
@@ -2016,7 +2052,7 @@ class ChatActivity :
 
     private fun isTypingStatusEnabled(): Boolean {
         return webSocketInstance != null &&
-            !CapabilitiesUtilNew.isTypingStatusPrivate(conversationUser!!)
+            !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!)
     }
 
     private fun setupSwipeToReply() {
@@ -2048,7 +2084,7 @@ class ChatActivity :
 
         if (isOneToOneConversation()) {
             var url = ApiUtils.getUrlForAvatar(
-                conversationUser!!.baseUrl,
+                conversationUser!!.baseUrl!!,
                 currentConversation!!.name,
                 true
             )
@@ -2097,18 +2133,19 @@ class ChatActivity :
             }
 
             val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
-
-            context.imageLoader.enqueue(
-                ImageRequest.Builder(context)
-                    .data(url)
-                    .addHeader("Authorization", credentials)
-                    .transformations(CircleCropTransformation())
-                    .crossfade(true)
-                    .target(target)
-                    .memoryCachePolicy(CachePolicy.DISABLED)
-                    .diskCachePolicy(CachePolicy.DISABLED)
-                    .build()
-            )
+            if (credentials != null) {
+                context.imageLoader.enqueue(
+                    ImageRequest.Builder(context)
+                        .data(url)
+                        .addHeader("Authorization", credentials)
+                        .transformations(CircleCropTransformation())
+                        .crossfade(true)
+                        .target(target)
+                        .memoryCachePolicy(CachePolicy.DISABLED)
+                        .diskCachePolicy(CachePolicy.DISABLED)
+                        .build()
+                )
+            }
         } else {
             binding.chatToolbar.findViewById<FrameLayout>(R.id.chat_toolbar_avatar_container).visibility = View.GONE
         }
@@ -2463,7 +2500,10 @@ class ChatActivity :
 
         val baseUrl = message.activeUser!!.baseUrl
         val userId = message.activeUser!!.userId
-        val attachmentFolder = CapabilitiesUtilNew.getAttachmentFolder(message.activeUser!!)
+        val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(
+            message.activeUser!!.capabilities!!
+                .spreedCapability!!
+        )
         val fileName = message.selectedIndividualHashMap!!["name"]
         var size = message.selectedIndividualHashMap!!["size"]
         if (size == null) {
@@ -2801,16 +2841,16 @@ class ChatActivity :
 
     private fun shouldShowLobby(): Boolean {
         if (currentConversation != null) {
-            return CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") &&
+            return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
                 currentConversation?.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
-                !ConversationUtils.canModerate(currentConversation!!, conversationUser!!) &&
+                !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
                 !participantPermissions.canIgnoreLobby()
         }
         return false
     }
 
     private fun disableCallButtons() {
-        if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
+        if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
             if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
                 conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
                 conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
@@ -2823,7 +2863,7 @@ class ChatActivity :
     }
 
     private fun enableCallButtons() {
-        if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
+        if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
             if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
                 conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
                 conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
@@ -2843,7 +2883,7 @@ class ChatActivity :
 
     private fun checkLobbyState() {
         if (currentConversation != null &&
-            ConversationUtils.isLobbyViewApplicable(currentConversation!!, conversationUser!!)
+            ConversationUtils.isLobbyViewApplicable(currentConversation!!, spreedCapabilities)
         ) {
             if (shouldShowLobby()) {
                 binding.lobby.lobbyView.visibility = View.VISIBLE
@@ -3252,6 +3292,7 @@ class ChatActivity :
 
         val intent = Intent(this, LocationPickerActivity::class.java)
         intent.putExtra(KEY_ROOM_TOKEN, roomToken)
+        intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
         startActivity(intent)
     }
 
@@ -3270,7 +3311,7 @@ class ChatActivity :
         val elevation = MENTION_AUTO_COMPLETE_ELEVATION
         resources?.let {
             val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default, null))
-            val presenter = MentionAutocompletePresenter(this, roomToken)
+            val presenter = MentionAutocompletePresenter(this, roomToken, chatApiVersion)
             val callback = MentionAutocompleteCallback(
                 this,
                 conversationUser!!,
@@ -3430,12 +3471,6 @@ class ChatActivity :
 
         if (!validSessionId()) {
             Log.d(TAG, "sessionID was not valid -> joinRoom")
-            var apiVersion = 1
-            // FIXME Fix API checking with guests?
-            if (conversationUser != null) {
-                apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
-            }
-
             val startNanoTime = System.nanoTime()
             Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime")
 
@@ -3458,7 +3493,7 @@ class ChatActivity :
         var apiVersion = 1
         // FIXME Fix API checking with guests?
         if (conversationUser != null) {
-            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
         }
 
         val startNanoTime = System.nanoTime()
@@ -3467,7 +3502,7 @@ class ChatActivity :
             credentials!!,
             ApiUtils.getUrlForParticipantsActive(
                 apiVersion,
-                conversationUser?.baseUrl,
+                conversationUser?.baseUrl!!,
                 roomToken
             ),
             funToCallWhenLeaveSuccessful
@@ -3513,11 +3548,9 @@ class ChatActivity :
 
     private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
         if (conversationUser != null) {
-            val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
-
             chatViewModel.sendChatMessage(
                 credentials!!,
-                ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl, roomToken),
+                ApiUtils.getUrlForChat(chatApiVersion, conversationUser!!.baseUrl!!, roomToken),
                 message,
                 conversationUser!!.displayName ?: "",
                 replyTo ?: 0,
@@ -3637,7 +3670,7 @@ class ChatActivity :
             }
         }
 
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
             deleteExpiredMessages()
         }
     }
@@ -3871,53 +3904,58 @@ class ChatActivity :
         if (conversationUser?.userId == "?") {
             menu.removeItem(R.id.conversation_info)
         } else {
-            conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
+            loadAvatarForStatusBar()
+            setActionBarTitle()
+        }
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        super.onPrepareOptionsMenu(menu)
 
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
+        if (this::spreedCapabilities.isInitialized) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
+                checkShowCallButtons()
+            }
+
+            val searchItem = menu.findItem(R.id.conversation_search)
+            searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities)
+
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                    spreedCapabilities,
+                    SpreedFeatures.RICH_OBJECT_LIST_MEDIA
+                )
+            ) {
                 conversationSharedItemsItem = menu.findItem(R.id.shared_items)
             } else {
                 menu.removeItem(R.id.shared_items)
             }
 
-            loadAvatarForStatusBar()
-            setActionBarTitle()
-        }
-
-        if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
-            conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
-            conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
+            if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
+                conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
+                conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
 
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-call")) {
-                Handler().post {
-                    findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
-                        showCallButtonMenu(true)
-                        true
+                if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
+                    Handler().post {
+                        findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
+                            showCallButtonMenu(true)
+                            true
+                        }
                     }
-                }
 
-                Handler().post {
-                    findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
-                        showCallButtonMenu(false)
-                        true
+                    Handler().post {
+                        findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
+                            showCallButtonMenu(false)
+                            true
+                        }
                     }
                 }
+            } else {
+                menu.removeItem(R.id.conversation_video_call)
+                menu.removeItem(R.id.conversation_voice_call)
             }
-        } else {
-            menu.removeItem(R.id.conversation_video_call)
-            menu.removeItem(R.id.conversation_voice_call)
         }
-        return true
-    }
 
-    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
-        super.onPrepareOptionsMenu(menu)
-        conversationUser?.let {
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(it, "read-only-rooms")) {
-                checkShowCallButtons()
-            }
-            val searchItem = menu.findItem(R.id.conversation_search)
-            searchItem.isVisible = CapabilitiesUtilNew.isUnifiedSearchAvailable(it)
-        }
         return true
     }
 
@@ -4054,7 +4092,7 @@ class ChatActivity :
     private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
         currentConversation?.let {
             if (conversationUser != null) {
-                val pp = ParticipantPermissions(conversationUser!!, it)
+                val pp = ParticipantPermissions(spreedCapabilities, it)
                 if (!pp.canStartCall() && currentConversation?.hasCall == false) {
                     Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show()
                 } else {
@@ -4074,7 +4112,7 @@ class ChatActivity :
             bundle.putString(KEY_ROOM_TOKEN, roomToken)
             bundle.putString(KEY_ROOM_ID, roomId)
             bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
-            bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
+            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, ConversationUtils.isParticipantOwnerOrModerator(it))
@@ -4147,7 +4185,8 @@ class ChatActivity :
                 conversationUser,
                 currentConversation,
                 isShowMessageDeletionButton(message),
-                participantPermissions.hasChatPermission()
+                participantPermissions.hasChatPermission(),
+                spreedCapabilities
             ).show()
         }
     }
@@ -4156,7 +4195,7 @@ class ChatActivity :
         return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
     }
 
-    fun deleteMessage(message: IMessage?) {
+    fun deleteMessage(message: IMessage) {
         if (!participantPermissions.hasChatPermission()) {
             Log.w(
                 TAG,
@@ -4168,28 +4207,28 @@ class ChatActivity :
             var apiVersion = 1
             // FIXME Fix API checking with guests?
             if (conversationUser != null) {
-                apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+                apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
             }
 
             chatViewModel.deleteChatMessages(
                 credentials!!,
                 ApiUtils.getUrlForChatMessage(
                     apiVersion,
-                    conversationUser?.baseUrl,
+                    conversationUser?.baseUrl!!,
                     roomToken,
-                    message?.id
+                    message.id!!
                 ),
-                message?.id!!
+                message.id!!
             )
         }
     }
 
     fun replyPrivately(message: IMessage?) {
         val apiVersion =
-            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+            ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
         val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
             apiVersion,
-            conversationUser?.baseUrl,
+            conversationUser?.baseUrl!!,
             "1",
             null,
             message?.user?.id?.substring(INVITE_LENGTH),
@@ -4215,10 +4254,14 @@ class ChatActivity :
 
     fun remindMeLater(message: ChatMessage?) {
         Log.d(TAG, "remindMeLater called")
+
+        val chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1, 1))
+
         val newFragment: DialogFragment = DateTimePickerFragment.newInstance(
             roomToken,
             message!!.id,
-            chatViewModel
+            chatViewModel,
+            chatApiVersion
         )
         newFragment.show(supportFragmentManager, DateTimePickerFragment.TAG)
     }
@@ -4229,8 +4272,8 @@ class ChatActivity :
             chatViewModel.setChatReadMarker(
                 credentials!!,
                 ApiUtils.getUrlForChatReadMarker(
-                    ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
-                    conversationUser?.baseUrl,
+                    ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1)),
+                    conversationUser?.baseUrl!!,
                     roomToken
                 ),
                 chatMessage.previousMessageId
@@ -4312,10 +4355,10 @@ class ChatActivity :
     }
 
     fun shareToNotes(message: ChatMessage, roomToken: String) {
-        val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+        val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
         val type = message.getCalculateMessageType()
         var shareUri: Uri? = null
-        var data: HashMap<String?, String?>?
+        val data: HashMap<String?, String?>?
         var metaData: String = ""
         var objectId: String = ""
         if (message.hasFileAttachment()) {
@@ -4335,9 +4378,9 @@ class ChatActivity :
         } else if (message.hasGeoLocation()) {
             data = message.messageParameters?.get("object")
             objectId = data?.get("id")!!
-            val name = data.get("name")!!
-            val lat = data.get("latitude")!!
-            val lon = data.get("longitude")!!
+            val name = data["name"]!!
+            val lat = data["latitude"]!!
+            val lon = data["longitude"]!!
             metaData =
                 "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," +
                 "\"longitude\":\"$lon\",\"name\":\"$name\"}"
@@ -4348,6 +4391,7 @@ class ChatActivity :
                 uploadFile(shareUri.toString(), true, token = roomToken)
                 Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
             }
+
             ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
                 val caption = if (message.message != "{file}") message.message else ""
                 if (null != shareUri) {
@@ -4364,25 +4408,28 @@ class ChatActivity :
                     }
                 }
             }
+
             ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> {
                 chatViewModel.shareLocationToNotes(
                     credentials!!,
-                    ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl, roomToken),
+                    ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl!!, roomToken),
                     "geo-location",
                     objectId,
                     metaData
                 )
                 Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
             }
+
             ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> {
                 chatViewModel.shareToNotes(
                     credentials!!,
-                    ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl, roomToken),
+                    ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl!!, roomToken),
                     message.message!!,
                     conversationUser!!.displayName!!
                 )
                 Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
             }
+
             else -> {}
         }
     }
@@ -4456,7 +4503,10 @@ class ChatActivity :
     }
 
     private fun showMicrophoneButton(show: Boolean) {
-        if (show && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
+        if (show && CapabilitiesUtil.hasSpreedFeatureCapability(
+                spreedCapabilities, SpreedFeatures.VOICE_MESSAGE_SHARING
+            )
+        ) {
             Log.d(TAG, "Microphone shown")
             binding.messageInputView.messageSendButton.visibility = View.GONE
             binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
@@ -4542,7 +4592,7 @@ class ChatActivity :
             !isUserAllowedByPrivileges -> false
             message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
             message.isDeleted -> false
-            !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
+            !CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
             !participantPermissions.hasChatPermission() -> false
             else -> true
         }
@@ -4554,7 +4604,7 @@ class ChatActivity :
         val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
             true
         } else {
-            ConversationUtils.canModerate(currentConversation!!, conversationUser!!)
+            ConversationUtils.canModerate(currentConversation!!, spreedCapabilities)
         }
         return isUserAllowedByPrivileges
     }
@@ -4628,12 +4678,12 @@ class ChatActivity :
             var apiVersion = 1
             // FIXME Fix API checking with guests?
             if (conversationUser != null) {
-                apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+                apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
             }
 
             val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
                 apiVersion,
-                conversationUser?.baseUrl,
+                conversationUser?.baseUrl!!,
                 "1",
                 null,
                 userMentionClickEvent.userId,

+ 11 - 3
app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt

@@ -22,6 +22,7 @@ package com.nextcloud.talk.chat.data
 
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.domain.ConversationModel
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomsOverall
@@ -33,10 +34,17 @@ import retrofit2.Response
 @Suppress("LongParameterList", "TooManyFunctions")
 interface ChatRepository {
     fun getRoom(user: User, roomToken: String): Observable<ConversationModel>
+    fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability>
     fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel>
-    fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder>
-    fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder>
-    fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall>
+    fun setReminder(
+        user: User,
+        roomToken: String,
+        messageId: String,
+        timeStamp: Int,
+        chatApiVersion: Int
+    ): Observable<Reminder>
+    fun getReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable<Reminder>
+    fun deleteReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable<GenericOverall>
     fun shareToNotes(
         credentials: String,
         url: String,

+ 42 - 18
app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt

@@ -24,6 +24,7 @@ import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.chat.data.ChatRepository
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.domain.ConversationModel
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomsOverall
@@ -35,55 +36,78 @@ import retrofit2.Response
 
 class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository {
     override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
-        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1))
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
+        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
 
         return ncApi.getRoom(
             credentials,
-            ApiUtils.getUrlForRoom(apiVersion, user.baseUrl, roomToken)
+            ApiUtils.getUrlForRoom(apiVersion, user.baseUrl!!, roomToken)
         ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) }
     }
 
+    override fun getCapabilities(user: User, roomToken: String): Observable<SpreedCapability> {
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
+        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
+
+        return ncApi.getRoomCapabilities(
+            credentials,
+            ApiUtils.getUrlForRoomCapabilities(apiVersion, user.baseUrl!!, roomToken)
+        ).map { it.ocs?.data }
+    }
+
     override fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable<ConversationModel> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
-        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, 1))
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
+        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1))
 
         return ncApi.joinRoom(
             credentials,
-            ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl, roomToken),
+            ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl!!, roomToken),
             roomPassword
         ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) }
     }
 
-    override fun setReminder(user: User, roomToken: String, messageId: String, timeStamp: Int): Observable<Reminder> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
-        val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
+    override fun setReminder(
+        user: User,
+        roomToken: String,
+        messageId: String,
+        timeStamp: Int,
+        chatApiVersion: Int
+    ): Observable<Reminder> {
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
         return ncApi.setReminder(
             credentials,
-            ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion),
+            ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion),
             timeStamp
         ).map {
             it.ocs!!.data
         }
     }
 
-    override fun getReminder(user: User, roomToken: String, messageId: String): Observable<Reminder> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
-        val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
+    override fun getReminder(
+        user: User,
+        roomToken: String,
+        messageId: String,
+        chatApiVersion: Int
+    ): Observable<Reminder> {
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
         return ncApi.getReminder(
             credentials,
-            ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion)
+            ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion)
         ).map {
             it.ocs!!.data
         }
     }
 
-    override fun deleteReminder(user: User, roomToken: String, messageId: String): Observable<GenericOverall> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
-        val apiVersion = ApiUtils.getChatApiVersion(user, intArrayOf(ApiUtils.APIv1, 1))
+    override fun deleteReminder(
+        user: User,
+        roomToken: String,
+        messageId: String,
+        chatApiVersion: Int
+    ): Observable<GenericOverall> {
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
         return ncApi.deleteReminder(
             credentials,
-            ApiUtils.getUrlForReminder(user, roomToken, messageId, apiVersion)
+            ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion)
         ).map {
             it
         }

+ 77 - 38
app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

@@ -31,6 +31,7 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.models.domain.ReactionAddedModel
 import com.nextcloud.talk.models.domain.ReactionDeletedModel
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.conversations.RoomOverall
@@ -105,6 +106,14 @@ class ChatViewModel @Inject constructor(
     val getRoomViewState: LiveData<ViewState>
         get() = _getRoomViewState
 
+    object GetCapabilitiesStartState : ViewState
+    object GetCapabilitiesErrorState : ViewState
+    open class GetCapabilitiesSuccessState(val spreedCapabilities: SpreedCapability) : ViewState
+
+    private val _getCapabilitiesViewState: MutableLiveData<ViewState> = MutableLiveData(GetCapabilitiesStartState)
+    val getCapabilitiesViewState: LiveData<ViewState>
+        get() = _getCapabilitiesViewState
+
     object JoinRoomStartState : ViewState
     object JoinRoomErrorState : ViewState
     open class JoinRoomSuccessState(val conversationModel: ConversationModel) : ViewState
@@ -184,6 +193,36 @@ class ChatViewModel @Inject constructor(
             ?.subscribe(GetRoomObserver())
     }
 
+    fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
+        _getCapabilitiesViewState.value = GetCapabilitiesStartState
+
+        if (conversationModel.remoteServer.isNullOrEmpty()) {
+            _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!)
+        } else {
+            chatRepository.getCapabilities(user, token)
+                .subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(object : Observer<SpreedCapability> {
+                    override fun onSubscribe(d: Disposable) {
+                        LifeCycleObserver.disposableSet.add(d)
+                    }
+
+                    override fun onNext(spreedCapabilities: SpreedCapability) {
+                        _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(spreedCapabilities)
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "Error when fetching spreed capabilities", e)
+                        _getCapabilitiesViewState.value = GetCapabilitiesErrorState
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+    }
+
     fun joinRoom(user: User, token: String, roomPassword: String) {
         _joinRoomViewState.value = JoinRoomStartState
         chatRepository.joinRoom(user, token, roomPassword)
@@ -193,6 +232,43 @@ class ChatViewModel @Inject constructor(
             ?.subscribe(JoinRoomObserver())
     }
 
+    fun setReminder(user: User, roomToken: String, messageId: String, timestamp: Int, chatApiVersion: Int) {
+        chatRepository.setReminder(user, roomToken, messageId, timestamp, chatApiVersion)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(SetReminderObserver())
+    }
+
+    fun getReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) {
+        chatRepository.getReminder(user, roomToken, messageId, chatApiVersion)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(GetReminderObserver())
+    }
+
+    fun deleteReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) {
+        chatRepository.deleteReminder(user, roomToken, messageId, chatApiVersion)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    LifeCycleObserver.disposableSet.add(d)
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    _getReminderExistState.value = GetReminderStartState
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.d(TAG, "Error when deleting reminder", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
     fun leaveRoom(credentials: String, url: String, funToCallWhenLeaveSuccessful: (() -> Unit)?) {
         val startNanoTime = System.nanoTime()
         chatRepository.leaveRoom(credentials, url)
@@ -357,43 +433,6 @@ class ChatViewModel @Inject constructor(
             })
     }
 
-    fun setReminder(user: User, roomToken: String, messageId: String, timestamp: Int) {
-        chatRepository.setReminder(user, roomToken, messageId, timestamp)
-            .subscribeOn(Schedulers.io())
-            ?.observeOn(AndroidSchedulers.mainThread())
-            ?.subscribe(SetReminderObserver())
-    }
-
-    fun getReminder(user: User, roomToken: String, messageId: String) {
-        chatRepository.getReminder(user, roomToken, messageId)
-            .subscribeOn(Schedulers.io())
-            ?.observeOn(AndroidSchedulers.mainThread())
-            ?.subscribe(GetReminderObserver())
-    }
-
-    fun deleteReminder(user: User, roomToken: String, messageId: String) {
-        chatRepository.deleteReminder(user, roomToken, messageId)
-            .subscribeOn(Schedulers.io())
-            ?.observeOn(AndroidSchedulers.mainThread())
-            ?.subscribe(object : Observer<GenericOverall> {
-                override fun onSubscribe(d: Disposable) {
-                    LifeCycleObserver.disposableSet.add(d)
-                }
-
-                override fun onNext(genericOverall: GenericOverall) {
-                    _getReminderExistState.value = GetReminderStartState
-                }
-
-                override fun onError(e: Throwable) {
-                    Log.d(TAG, "Error when deleting reminder $e")
-                }
-
-                override fun onComplete() {
-                    // unused atm
-                }
-            })
-    }
-
     fun shareToNotes(credentials: String, url: String, message: String, displayName: String) {
         chatRepository.shareToNotes(credentials, url, message, displayName)
             .subscribeOn(Schedulers.io())
@@ -522,7 +561,7 @@ class ChatViewModel @Inject constructor(
 
     inner class GetRoomObserver : Observer<ConversationModel> {
         override fun onSubscribe(d: Disposable) {
-            LifeCycleObserver.disposableSet.add(d)
+            // unused atm
         }
 
         override fun onNext(conversationModel: ConversationModel) {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFolderListingOperation.kt

@@ -65,7 +65,7 @@ class ReadFolderListingOperation(okHttpClient: OkHttpClient, currentUser: User,
                 ApiUtils.getCredentials(
                     currentUser.username,
                     currentUser.token
-                ),
+                )!!,
                 "Authorization"
             )
         )

+ 23 - 10
app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt

@@ -68,9 +68,10 @@ import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.openconversations.ListOpenConversationsActivity
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.UserIdUtils.getIdForUser
 import com.nextcloud.talk.utils.bundle.BundleKeys
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.SelectableAdapter
 import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
@@ -318,10 +319,10 @@ class ContactsActivity :
     }
 
     private fun createRoom(roomType: String, sourceType: String?, userId: String) {
-        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser!!, intArrayOf(ApiUtils.API_V4, 1))
         val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
             apiVersion,
-            currentUser!!.baseUrl,
+            currentUser!!.baseUrl!!,
             roomType,
             sourceType,
             userId,
@@ -438,7 +439,7 @@ class ContactsActivity :
         userHeaderItems = HashMap()
         val query = adapter!!.getFilter(String::class.java)
         val retrofitBucket: RetrofitBucket =
-            ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query)
+            ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl!!, query)
         val modifiedQueryMap: HashMap<String, Any?> = HashMap(retrofitBucket.queryMap)
         modifiedQueryMap["limit"] = CONTACTS_BATCH_SIZE
         if (isAddingParticipantsView) {
@@ -450,13 +451,21 @@ class ContactsActivity :
         if (!isAddingParticipantsView) {
             // groups
             shareTypesList.add("1")
-        } else if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
+        } else if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser?.capabilities?.spreedCapability!!,
+                SpreedFeatures.INVITE_GROUPS_AND_MAILS
+            )
+        ) {
             // groups
             shareTypesList.add("1")
             // emails
             shareTypesList.add("4")
         }
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "circles-support")) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser?.capabilities?.spreedCapability!!,
+                SpreedFeatures.CIRCLES_SUPPORT
+            )
+        ) {
             // circles
             shareTypesList.add("7")
         }
@@ -745,8 +754,12 @@ class ContactsActivity :
     private fun updateSelection(contactItem: ContactItem) {
         contactItem.model.selected = !contactItem.model.selected
         updateSelectionLists(contactItem.model)
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "last-room-activity") &&
-            !CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") &&
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser?.capabilities?.spreedCapability!!, SpreedFeatures.LAST_ROOM_ACTIVITY
+            ) &&
+            !CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser?.capabilities?.spreedCapability!!, SpreedFeatures.INVITE_GROUPS_AND_MAILS
+            ) &&
             isValidGroupSelection(contactItem, contactItem.model, adapter)
         ) {
             val currentItems: List<ContactItem> = adapter?.currentItems as List<ContactItem>
@@ -771,10 +784,10 @@ class ContactsActivity :
         if ("groups" == contactItem.model.source) {
             roomType = "2"
         }
-        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser!!, intArrayOf(ApiUtils.API_V4, 1))
         val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
             apiVersion,
-            currentUser!!.baseUrl,
+            currentUser!!.baseUrl!!,
             roomType,
             null,
             contactItem.model.calculatedActorId,

+ 6 - 6
app/src/main/java/com/nextcloud/talk/conversation/repository/ConversationRepositoryImpl.kt

@@ -36,16 +36,16 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
     ConversationRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
     override fun renameConversation(roomToken: String, roomNameNew: String): Observable<GenericOverall> {
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
 
         return ncApi.renameRoom(
             credentials,
             ApiUtils.getUrlForRoom(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken
             ),
             roomNameNew
@@ -59,12 +59,12 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
         roomName: String,
         conversationType: Conversation.ConversationType?
     ): Observable<RoomOverall> {
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
 
         val retrofitBucket: RetrofitBucket = if (conversationType == Conversation.ConversationType.ROOM_PUBLIC_CALL) {
             ApiUtils.getRetrofitBucketForCreateRoom(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 ROOM_TYPE_PUBLIC,
                 null,
                 null,
@@ -73,7 +73,7 @@ class ConversationRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
         } else {
             ApiUtils.getRetrofitBucketForCreateRoom(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 ROOM_TYPE_GROUP,
                 null,
                 null,

+ 196 - 165
app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt

@@ -40,6 +40,7 @@ import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.ViewModelProvider
 import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
@@ -61,6 +62,7 @@ import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage
 import com.nextcloud.talk.bottomsheet.items.listItemsWithImage
 import com.nextcloud.talk.contacts.ContactsActivity
 import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity
+import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
 import com.nextcloud.talk.events.EventStatus
@@ -71,9 +73,11 @@ import com.nextcloud.talk.extensions.loadUserAvatar
 import com.nextcloud.talk.jobs.DeleteConversationWorker
 import com.nextcloud.talk.jobs.LeaveConversationWorker
 import com.nextcloud.talk.models.domain.ConversationModel
-import com.nextcloud.talk.models.json.conversations.Conversation
-import com.nextcloud.talk.models.json.conversations.RoomOverall
-import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
+import com.nextcloud.talk.models.domain.ConversationType
+import com.nextcloud.talk.models.domain.LobbyState
+import com.nextcloud.talk.models.domain.NotificationLevel
+import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES
@@ -83,11 +87,12 @@ import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.repositories.conversations.ConversationsRepository
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ConversationUtils
 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.CapabilitiesUtil
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
 import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -109,6 +114,9 @@ class ConversationInfoActivity :
     FlexibleAdapter.OnItemClickListener {
     private lateinit var binding: ActivityConversationInfoBinding
 
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
     @Inject
     lateinit var ncApi: NcApi
 
@@ -121,6 +129,10 @@ class ConversationInfoActivity :
     @Inject
     lateinit var dateUtils: DateUtils
 
+    lateinit var viewModel: ConversationInfoViewModel
+
+    private lateinit var spreedCapabilities: SpreedCapability
+
     private lateinit var conversationToken: String
     private lateinit var conversationUser: User
     private var hasAvatarSpacing: Boolean = false
@@ -129,7 +141,9 @@ class ConversationInfoActivity :
     private var participantsDisposable: Disposable? = null
 
     private var databaseStorageModule: DatabaseStorageModule? = null
-    private var conversation: Conversation? = null
+
+    // private var conversation: Conversation? = null
+    private var conversation: ConversationModel? = null
 
     private var adapter: FlexibleAdapter<ParticipantItem>? = null
     private var userItems: MutableList<ParticipantItem> = ArrayList()
@@ -157,11 +171,26 @@ class ConversationInfoActivity :
         setContentView(binding.root)
         setupSystemColors()
 
+        viewModel =
+            ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
+
         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)
+        credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!!
+
+        initObservers()
+    }
+
+    override fun onStart() {
+        super.onStart()
+        this.lifecycle.addObserver(ConversationInfoViewModel.LifeCycleObserver)
+    }
+
+    override fun onStop() {
+        super.onStop()
+        this.lifecycle.removeObserver(ConversationInfoViewModel.LifeCycleObserver)
     }
 
     override fun onResume() {
@@ -176,13 +205,7 @@ class ConversationInfoActivity :
         binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() }
         binding.addParticipantsAction.setOnClickListener { addParticipants() }
 
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
-            binding.sharedItemsButton.setOnClickListener { showSharedItems() }
-        } else {
-            binding.sharedItems.visibility = GONE
-        }
-
-        fetchRoomInfo()
+        viewModel.getRoom(conversationUser, conversationToken)
 
         themeTextViews()
         themeSwitchPreferences()
@@ -192,6 +215,35 @@ class ConversationInfoActivity :
         binding.progressBar.let { viewThemeUtils.platform.colorCircularProgressBar(it, ColorRole.PRIMARY) }
     }
 
+    private fun initObservers() {
+        viewModel.viewState.observe(this) { state ->
+            when (state) {
+                is ConversationInfoViewModel.GetRoomSuccessState -> {
+                    conversation = state.conversationModel
+                    viewModel.getCapabilities(conversationUser, conversationToken, conversation!!)
+                }
+
+                is ConversationInfoViewModel.GetRoomErrorState -> {
+                    Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
+                }
+
+                else -> {}
+            }
+        }
+
+        viewModel.getCapabilitiesViewState.observe(this) { state ->
+            when (state) {
+                is ConversationInfoViewModel.GetCapabilitiesSuccessState -> {
+                    spreedCapabilities = state.spreedCapabilities
+
+                    handleConversation()
+                }
+
+                else -> {}
+            }
+        }
+    }
+
     private fun setupActionBar() {
         setSupportActionBar(binding.conversationInfoToolbar)
         binding.conversationInfoToolbar.setNavigationOnClickListener {
@@ -217,7 +269,7 @@ class ConversationInfoActivity :
     fun showOptionsMenu() {
         if (::optionsMenu.isInitialized) {
             optionsMenu.clear()
-            if (CapabilitiesUtilNew.isConversationAvatarEndpointAvailable(conversationUser)) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) {
                 menuInflater.inflate(R.menu.menu_conversation_info, optionsMenu)
             }
         }
@@ -273,19 +325,22 @@ 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(SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, conversation?.isParticipantOwnerOrModerator)
+        intent.putExtra(
+            SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
+            ConversationUtils.isParticipantOwnerOrModerator(conversation!!)
+        )
         startActivity(intent)
     }
 
     private fun setupWebinaryView() {
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") &&
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
             webinaryRoomType(conversation!!) &&
-            conversation!!.canModerate(conversationUser)
+            ConversationUtils.canModerate(conversation!!, spreedCapabilities)
         ) {
             binding.webinarInfoView.webinarSettings.visibility = VISIBLE
 
             val isLobbyOpenToModeratorsOnly =
-                conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
+                conversation!!.lobbyState == LobbyState.LOBBY_STATE_MODERATORS_ONLY
             binding.webinarInfoView.lobbySwitch.isChecked = isLobbyOpenToModeratorsOnly
 
             reconfigureLobbyTimerView()
@@ -320,9 +375,9 @@ class ConversationInfoActivity :
         }
     }
 
-    private fun webinaryRoomType(conversation: Conversation): Boolean {
-        return conversation.type == Conversation.ConversationType.ROOM_GROUP_CALL ||
-            conversation.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
+    private fun webinaryRoomType(conversation: ConversationModel): Boolean {
+        return conversation.type == ConversationType.ROOM_GROUP_CALL ||
+            conversation.type == ConversationType.ROOM_PUBLIC_CALL
     }
 
     private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
@@ -337,9 +392,9 @@ class ConversationInfoActivity :
         }
 
         conversation!!.lobbyState = if (isChecked) {
-            Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
+            LobbyState.LOBBY_STATE_MODERATORS_ONLY
         } else {
-            Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
+            LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
         }
 
         if (
@@ -370,11 +425,11 @@ class ConversationInfoActivity :
             0
         }
 
-        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
 
         ncApi.setLobbyForConversation(
             ApiUtils.getCredentials(conversationUser.username, conversationUser.token),
-            ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl, conversation!!.token),
+            ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl!!, conversation!!.token),
             state,
             conversation!!.lobbyTimer
         )
@@ -487,7 +542,7 @@ class ConversationInfoActivity :
 
     private fun getListOfParticipants() {
         // FIXME Fix API checking with guests?
-        val apiVersion: Int = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion: Int = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
 
         val fieldMap = HashMap<String, Boolean>()
         fieldMap["includeStatus"] = true
@@ -496,7 +551,7 @@ class ConversationInfoActivity :
             credentials,
             ApiUtils.getUrlForParticipants(
                 apiVersion,
-                conversationUser.baseUrl,
+                conversationUser.baseUrl!!,
                 conversationToken
             ),
             fieldMap
@@ -586,11 +641,11 @@ class ConversationInfoActivity :
     }
 
     private fun clearHistory() {
-        val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+        val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
 
         ncApi.clearChatHistory(
             credentials,
-            ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, conversationToken)
+            ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl!!, conversationToken)
         )
             ?.subscribeOn(Schedulers.io())
             ?.observeOn(AndroidSchedulers.mainThread())
@@ -631,123 +686,99 @@ class ConversationInfoActivity :
         }
     }
 
-    private fun fetchRoomInfo() {
-        val apiVersion: Int
-        // FIXME Fix API checking with guests?
-        apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
-
-        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, conversationToken))
-            ?.subscribeOn(Schedulers.io())
-            ?.observeOn(AndroidSchedulers.mainThread())
-            ?.subscribe(object : Observer<RoomOverall> {
-                override fun onSubscribe(d: Disposable) {
-                    roomDisposable = d
-                }
-
-                @Suppress("Detekt.TooGenericExceptionCaught")
-                override fun onNext(roomOverall: RoomOverall) {
-                    conversation = roomOverall.ocs!!.data
-
-                    val conversationCopy = conversation
+    @Suppress("LongMethod")
+    private fun handleConversation() {
+        val conversationCopy = conversation!!
 
-                    if (conversationCopy!!.canModerate(conversationUser)) {
-                        binding.addParticipantsAction.visibility = VISIBLE
-                        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(
-                                conversationUser,
-                                "clear-history"
-                            )
-                        ) {
-                            binding.clearConversationHistory.visibility = VISIBLE
-                        } else {
-                            binding.clearConversationHistory.visibility = GONE
-                        }
-                        showOptionsMenu()
-                    } else {
-                        binding.addParticipantsAction.visibility = GONE
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA)) {
+            binding.sharedItemsButton.setOnClickListener { showSharedItems() }
+        } else {
+            binding.sharedItems.visibility = GONE
+        }
 
-                        if (ConversationUtils.isNoteToSelfConversation(
-                                ConversationModel.mapToConversationModel(conversation!!)
-                            )
-                        ) {
-                            binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
-                        } else {
-                            binding.clearConversationHistory.visibility = GONE
-                        }
-                    }
+        if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
+            binding.addParticipantsAction.visibility = VISIBLE
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                    spreedCapabilities,
+                    SpreedFeatures.CLEAR_HISTORY
+                )
+            ) {
+                binding.clearConversationHistory.visibility = VISIBLE
+            } else {
+                binding.clearConversationHistory.visibility = GONE
+            }
+            showOptionsMenu()
+        } else {
+            binding.addParticipantsAction.visibility = GONE
 
-                    if (!isDestroyed) {
-                        binding.dangerZoneOptions.visibility = VISIBLE
+            if (ConversationUtils.isNoteToSelfConversation(conversation)) {
+                binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
+            } else {
+                binding.clearConversationHistory.visibility = GONE
+            }
+        }
 
-                        setupWebinaryView()
+        if (!isDestroyed) {
+            binding.dangerZoneOptions.visibility = VISIBLE
 
-                        if (!conversation!!.canLeave()) {
-                            binding.leaveConversationAction.visibility = GONE
-                        } else {
-                            binding.leaveConversationAction.visibility = VISIBLE
-                        }
+            setupWebinaryView()
 
-                        if (!conversation!!.canDelete(conversationUser)) {
-                            binding.deleteConversationAction.visibility = GONE
-                        } else {
-                            binding.deleteConversationAction.visibility = VISIBLE
-                        }
+            if (ConversationUtils.canLeave(conversation!!)) {
+                binding.leaveConversationAction.visibility = GONE
+            } else {
+                binding.leaveConversationAction.visibility = VISIBLE
+            }
 
-                        if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) {
-                            binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
-                        }
+            if (ConversationUtils.canDelete(conversation!!, spreedCapabilities)) {
+                binding.deleteConversationAction.visibility = GONE
+            } else {
+                binding.deleteConversationAction.visibility = VISIBLE
+            }
 
-                        if (conversation!!.notificationCalls === null) {
-                            binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
-                        } else {
-                            binding.notificationSettingsView.callNotificationsSwitch.isChecked =
-                                (conversationCopy.notificationCalls == 1)
-                        }
+            if (ConversationType.ROOM_SYSTEM == conversation!!.type) {
+                binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
+            }
 
-                        getListOfParticipants()
+            if (conversation!!.notificationCalls === null) {
+                binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
+            } else {
+                binding.notificationSettingsView.callNotificationsSwitch.isChecked =
+                    (conversationCopy.notificationCalls == 1)
+            }
 
-                        binding.progressBar.visibility = GONE
+            getListOfParticipants()
 
-                        binding.conversationInfoName.visibility = VISIBLE
+            binding.progressBar.visibility = GONE
 
-                        binding.displayNameText.text = conversation!!.displayName
+            binding.conversationInfoName.visibility = VISIBLE
 
-                        if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) {
-                            binding.descriptionText.text = conversation!!.description
-                            binding.conversationDescription.visibility = VISIBLE
-                        }
+            binding.displayNameText.text = conversation!!.displayName
 
-                        loadConversationAvatar()
-                        adjustNotificationLevelUI()
-                        initRecordingConsentOption()
-                        initExpiringMessageOption()
-
-                        binding.let {
-                            GuestAccessHelper(
-                                this@ConversationInfoActivity,
-                                it,
-                                conversation!!,
-                                conversationUser
-                            ).setupGuestAccess()
-                        }
-                        if (ConversationUtils.isNoteToSelfConversation(
-                                ConversationModel.mapToConversationModel(conversation!!)
-                            )
-                        ) {
-                            binding.notificationSettingsView.notificationSettings.visibility = GONE
-                        } else {
-                            binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
-                        }
-                    }
-                }
+            if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) {
+                binding.descriptionText.text = conversation!!.description
+                binding.conversationDescription.visibility = VISIBLE
+            }
 
-                override fun onError(e: Throwable) {
-                    Log.e(TAG, "failed to fetch room info", e)
-                }
+            loadConversationAvatar()
+            adjustNotificationLevelUI()
+            initRecordingConsentOption()
+            initExpiringMessageOption()
 
-                override fun onComplete() {
-                    roomDisposable!!.dispose()
-                }
-            })
+            binding.let {
+                GuestAccessHelper(
+                    this@ConversationInfoActivity,
+                    it,
+                    conversation!!,
+                    spreedCapabilities,
+                    conversationUser
+                ).setupGuestAccess()
+            }
+            if (ConversationUtils.isNoteToSelfConversation(conversation!!)) {
+                binding.notificationSettingsView.notificationSettings.visibility = GONE
+            } else {
+                binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
+            }
+        }
     }
 
     private fun initRecordingConsentOption() {
@@ -781,13 +812,13 @@ class ConversationInfoActivity :
             }
         }
 
-        if (conversation!!.isParticipantOwnerOrModerator &&
-            !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(conversation!!))
+        if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) &&
+            !ConversationUtils.isNoteToSelfConversation(conversation!!)
         ) {
-            when (CapabilitiesUtilNew.getRecordingConsentType(conversationUser)) {
-                CapabilitiesUtilNew.RECORDING_CONSENT_NOT_REQUIRED -> hide()
-                CapabilitiesUtilNew.RECORDING_CONSENT_REQUIRED -> showAlwaysRequiredInfo()
-                CapabilitiesUtilNew.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> showSwitch()
+            when (CapabilitiesUtil.getRecordingConsentType(spreedCapabilities)) {
+                CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED -> hide()
+                CapabilitiesUtil.RECORDING_CONSENT_REQUIRED -> showAlwaysRequiredInfo()
+                CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> showSwitch()
             }
         } else {
             hide()
@@ -801,11 +832,11 @@ class ConversationInfoActivity :
             RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION
         }
 
-        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
 
         ncApi.setRecordingConsent(
             ApiUtils.getCredentials(conversationUser.username, conversationUser.token),
-            ApiUtils.getUrlForRecordingConsent(apiVersion, conversationUser.baseUrl, conversation!!.token),
+            ApiUtils.getUrlForRecordingConsent(apiVersion, conversationUser.baseUrl!!, conversation!!.token),
             state
         )
             ?.subscribeOn(Schedulers.io())
@@ -831,8 +862,8 @@ class ConversationInfoActivity :
     }
 
     private fun initExpiringMessageOption() {
-        if (conversation!!.isParticipantOwnerOrModerator &&
-            CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")
+        if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) &&
+            CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)
         ) {
             databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration)
             val value = databaseStorageModule!!.getString("conversation_settings_dropdown", "")
@@ -855,13 +886,16 @@ class ConversationInfoActivity :
 
     private fun adjustNotificationLevelUI() {
         if (conversation != null) {
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "notification-levels")) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) {
                 binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true
                 binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f
 
-                if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
+                if (conversation!!.notificationLevel != NotificationLevel.DEFAULT) {
                     val stringValue: String =
-                        when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
+                        when (
+                            DomainEnumNotificationLevelConverter()
+                                .convertToInt(conversation!!.notificationLevel!!)
+                        ) {
                             NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always)
                             NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention)
                             NOTIFICATION_LEVEL_NEVER -> resources.getString(R.string.nc_notify_me_never)
@@ -885,9 +919,9 @@ class ConversationInfoActivity :
         }
     }
 
-    private fun setProperNotificationValue(conversation: Conversation?) {
-        if (conversation!!.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag")) {
+    private fun setProperNotificationValue(conversation: ConversationModel?) {
+        if (conversation!!.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) {
                 binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText(
                     resources.getString(R.string.nc_notify_me_always)
                 )
@@ -905,7 +939,7 @@ class ConversationInfoActivity :
 
     private fun loadConversationAvatar() {
         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,
@@ -916,7 +950,7 @@ class ConversationInfoActivity :
                 }
             }
 
-            Conversation.ConversationType.ROOM_GROUP_CALL, Conversation.ConversationType.ROOM_PUBLIC_CALL -> {
+            ConversationType.ROOM_GROUP_CALL, ConversationType.ROOM_PUBLIC_CALL -> {
                 binding.avatarImage.loadConversationAvatar(
                     conversationUser,
                     conversation!!,
@@ -925,15 +959,12 @@ class ConversationInfoActivity :
                 )
             }
 
-            Conversation.ConversationType.ROOM_SYSTEM -> {
+            ConversationType.ROOM_SYSTEM -> {
                 binding.avatarImage.loadSystemAvatar()
             }
 
-            Conversation.ConversationType.DUMMY -> {
-                if (ConversationUtils.isNoteToSelfConversation(
-                        ConversationModel.mapToConversationModel(conversation!!)
-                    )
-                ) {
+            ConversationType.DUMMY -> {
+                if (ConversationUtils.isNoteToSelfConversation(conversation!!)) {
                     binding.avatarImage.loadNoteToSelfAvatar()
                 }
             }
@@ -971,7 +1002,7 @@ class ConversationInfoActivity :
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
-                    conversationUser.baseUrl,
+                    conversationUser.baseUrl!!,
                     conversation!!.token
                 ),
                 participant.attendeeId
@@ -986,7 +1017,7 @@ class ConversationInfoActivity :
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
-                    conversationUser.baseUrl,
+                    conversationUser.baseUrl!!,
                     conversation!!.token
                 ),
                 participant.attendeeId
@@ -1022,7 +1053,7 @@ class ConversationInfoActivity :
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
-                    conversationUser.baseUrl,
+                    conversationUser.baseUrl!!,
                     conversation!!.token
                 ),
                 participant.userId
@@ -1035,7 +1066,7 @@ class ConversationInfoActivity :
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
-                    conversationUser.baseUrl,
+                    conversationUser.baseUrl!!,
                     conversation!!.token
                 ),
                 participant.userId
@@ -1047,12 +1078,12 @@ class ConversationInfoActivity :
     }
 
     private fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
-        if (apiVersion >= ApiUtils.APIv4) {
+        if (apiVersion >= ApiUtils.API_V4) {
             ncApi.removeAttendeeFromConversation(
                 credentials,
                 ApiUtils.getUrlForAttendees(
                     apiVersion,
-                    conversationUser.baseUrl,
+                    conversationUser.baseUrl!!,
                     conversation!!.token
                 ),
                 participant.attendeeId
@@ -1084,7 +1115,7 @@ class ConversationInfoActivity :
                 ncApi.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
-                        conversationUser.baseUrl,
+                        conversationUser.baseUrl!!,
                         conversation!!.token,
                         true
                     ),
@@ -1114,7 +1145,7 @@ class ConversationInfoActivity :
                 ncApi.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
-                        conversationUser.baseUrl,
+                        conversationUser.baseUrl!!,
                         conversation!!.token,
                         false
                     ),
@@ -1146,14 +1177,14 @@ class ConversationInfoActivity :
 
     @SuppressLint("CheckResult")
     override fun onItemClick(view: View?, position: Int): Boolean {
-        if (!conversation!!.canModerate(conversationUser)) {
+        if (ConversationUtils.canModerate(conversation!!, spreedCapabilities)) {
             return true
         }
 
         val userItem = adapter?.getItem(position) as ParticipantItem
         val participant = userItem.model
 
-        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
 
         if (participant.calculatedActorType == USERS && participant.calculatedActorId == conversationUser.userId) {
             if (participant.attendeePin?.isNotEmpty() == true) {
@@ -1277,7 +1308,7 @@ class ConversationInfoActivity :
                         // Pin, nothing to do
                     } else if (actionToTrigger == 1) {
                         // Promote/demote
-                        if (apiVersion >= ApiUtils.APIv4) {
+                        if (apiVersion >= ApiUtils.API_V4) {
                             toggleModeratorStatus(apiVersion, participant)
                         } else {
                             toggleModeratorStatusLegacy(apiVersion, participant)

+ 8 - 4
app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt

@@ -11,8 +11,11 @@ import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
 import com.nextcloud.talk.databinding.DialogPasswordBinding
-import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.domain.ConversationModel
+import com.nextcloud.talk.models.domain.ConversationType
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.repositories.conversations.ConversationsRepository
+import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.Mimetype
 import com.nextcloud.talk.utils.ShareUtils
 import io.reactivex.Observer
@@ -23,7 +26,8 @@ import io.reactivex.schedulers.Schedulers
 class GuestAccessHelper(
     private val activity: ConversationInfoActivity,
     private val binding: ActivityConversationInfoBinding,
-    private val conversation: Conversation,
+    private val conversation: ConversationModel,
+    private val spreedCapabilities: SpreedCapability,
     private val conversationUser: User
 ) {
 
@@ -32,13 +36,13 @@ class GuestAccessHelper(
     private val context = activity.context
 
     fun setupGuestAccess() {
-        if (conversation.canModerate(conversationUser)) {
+        if (ConversationUtils.canModerate(conversation, spreedCapabilities)) {
             binding.guestAccessView.guestAccessSettings.visibility = View.VISIBLE
         } else {
             binding.guestAccessView.guestAccessSettings.visibility = View.GONE
         }
 
-        if (conversation.type == Conversation.ConversationType.ROOM_PUBLIC_CALL) {
+        if (conversation.type == ConversationType.ROOM_PUBLIC_CALL) {
             binding.guestAccessView.allowGuestsSwitch.isChecked = true
             showAllOptions()
             if (conversation.hasPassword) {

+ 142 - 0
app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt

@@ -0,0 +1,142 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.conversationinfo.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+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 com.nextcloud.talk.models.json.capabilities.SpreedCapability
+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 ConversationInfoViewModel @Inject constructor(
+    private val chatRepository: ChatRepository
+) : ViewModel() {
+
+    object LifeCycleObserver : DefaultLifecycleObserver {
+        enum class LifeCycleFlag {
+            PAUSED,
+            RESUMED
+        }
+        lateinit var currentLifeCycleFlag: LifeCycleFlag
+        public val disposableSet = mutableSetOf<Disposable>()
+
+        override fun onResume(owner: LifecycleOwner) {
+            super.onResume(owner)
+            currentLifeCycleFlag = LifeCycleFlag.RESUMED
+        }
+
+        override fun onPause(owner: LifecycleOwner) {
+            super.onPause(owner)
+            currentLifeCycleFlag = LifeCycleFlag.PAUSED
+            disposableSet.forEach { disposable -> disposable.dispose() }
+            disposableSet.clear()
+        }
+    }
+
+    sealed interface ViewState
+
+    object GetRoomStartState : ViewState
+    object GetRoomErrorState : ViewState
+    open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(GetRoomStartState)
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    object GetCapabilitiesStartState : ViewState
+    object GetCapabilitiesErrorState : ViewState
+    open class GetCapabilitiesSuccessState(val spreedCapabilities: SpreedCapability) : ViewState
+
+    private val _getCapabilitiesViewState: MutableLiveData<ViewState> = MutableLiveData(GetCapabilitiesStartState)
+    val getCapabilitiesViewState: LiveData<ViewState>
+        get() = _getCapabilitiesViewState
+
+    fun getRoom(user: User, token: String) {
+        _viewState.value = GetRoomStartState
+        chatRepository.getRoom(user, token)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(GetRoomObserver())
+    }
+
+    fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
+        _getCapabilitiesViewState.value = GetCapabilitiesStartState
+
+        if (conversationModel.remoteServer.isNullOrEmpty()) {
+            _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!)
+        } else {
+            chatRepository.getCapabilities(user, token)
+                .subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(object : Observer<SpreedCapability> {
+                    override fun onSubscribe(d: Disposable) {
+                        LifeCycleObserver.disposableSet.add(d)
+                    }
+
+                    override fun onNext(spreedCapabilities: SpreedCapability) {
+                        _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(spreedCapabilities)
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "Error when fetching spreed capabilities", e)
+                        _getCapabilitiesViewState.value = GetCapabilitiesErrorState
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
+        }
+    }
+
+    inner class GetRoomObserver : Observer<ConversationModel> {
+        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
+        }
+    }
+
+    companion object {
+        private val TAG = ConversationInfoViewModel::class.simpleName
+    }
+}

+ 13 - 8
app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt

@@ -49,11 +49,12 @@ import com.nextcloud.talk.extensions.loadSystemAvatar
 import com.nextcloud.talk.extensions.loadUserAvatar
 import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.models.domain.ConversationType
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.CapabilitiesUtil
 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
@@ -87,6 +88,8 @@ class ConversationInfoEditActivity :
 
     private lateinit var pickImage: PickImage
 
+    private lateinit var spreedCapabilities: SpreedCapability
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@@ -110,7 +113,7 @@ class ConversationInfoEditActivity :
         viewThemeUtils.material.colorTextInputLayout(binding.conversationNameInputLayout)
         viewThemeUtils.material.colorTextInputLayout(binding.conversationDescriptionInputLayout)
 
-        credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)
+        credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!!
 
         pickImage = PickImage(this, conversationUser)
 
@@ -127,13 +130,15 @@ class ConversationInfoEditActivity :
                 is ConversationInfoEditViewModel.GetRoomSuccessState -> {
                     conversation = state.conversationModel
 
+                    spreedCapabilities = conversationUser.capabilities!!.spreedCapability!!
+
                     binding.conversationName.setText(conversation!!.displayName)
 
                     if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) {
                         binding.conversationDescription.setText(conversation!!.description)
                     }
 
-                    if (!CapabilitiesUtilNew.isConversationDescriptionEndpointAvailable(conversationUser)) {
+                    if (!CapabilitiesUtil.isConversationDescriptionEndpointAvailable(spreedCapabilities)) {
                         binding.conversationDescription.isEnabled = false
                     }
 
@@ -221,13 +226,13 @@ class ConversationInfoEditActivity :
 
     private fun saveConversationNameAndDescription() {
         val apiVersion =
-            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
 
         ncApi.renameRoom(
             credentials,
             ApiUtils.getUrlForRoom(
                 apiVersion,
-                conversationUser.baseUrl,
+                conversationUser.baseUrl!!,
                 conversation!!.token
             ),
             binding.conversationName.text.toString()
@@ -241,7 +246,7 @@ class ConversationInfoEditActivity :
                 }
 
                 override fun onNext(genericOverall: GenericOverall) {
-                    if (CapabilitiesUtilNew.isConversationDescriptionEndpointAvailable(conversationUser)) {
+                    if (CapabilitiesUtil.isConversationDescriptionEndpointAvailable(spreedCapabilities)) {
                         saveConversationDescription()
                     } else {
                         finish()
@@ -265,13 +270,13 @@ class ConversationInfoEditActivity :
 
     fun saveConversationDescription() {
         val apiVersion =
-            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
 
         ncApi.setConversationDescription(
             credentials,
             ApiUtils.getUrlForConversationDescription(
                 apiVersion,
-                conversationUser.baseUrl,
+                conversationUser.baseUrl!!,
                 conversation!!.token
             ),
             binding.conversationDescription.text.toString()

+ 4 - 4
app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt

@@ -36,9 +36,9 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr
     ConversationInfoEditRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
-    val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1))
+    val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
 
     override fun uploadConversationAvatar(user: User, file: File, roomToken: String): Observable<ConversationModel> {
         val builder = MultipartBody.Builder()
@@ -56,7 +56,7 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr
 
         return ncApi.uploadConversationAvatar(
             credentials,
-            ApiUtils.getUrlForConversationAvatar(1, user.baseUrl, roomToken),
+            ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken),
             filePart
         ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) }
     }
@@ -64,7 +64,7 @@ class ConversationInfoEditRepositoryImpl(private val ncApi: NcApi, currentUserPr
     override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> {
         return ncApi.deleteConversationAvatar(
             credentials,
-            ApiUtils.getUrlForConversationAvatar(1, user.baseUrl, roomToken)
+            ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken)
         ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!) }
     }
 }

+ 30 - 15
app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

@@ -115,6 +115,7 @@ import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
 import com.nextcloud.talk.ui.dialog.FilterConversationFragment
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.Mimetype
@@ -130,10 +131,10 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NEW_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_SHARED_TEXT
-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
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUserStatusAvailable
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
+import com.nextcloud.talk.utils.CapabilitiesUtil.isServerEOL
+import com.nextcloud.talk.utils.CapabilitiesUtil.isUnifiedSearchAvailable
+import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.power.PowerManagerUtils
 import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
@@ -283,11 +284,11 @@ class ConversationsListActivity :
         }
         currentUser = userManager.currentUser.blockingGet()
         if (currentUser != null) {
-            if (isServerEOL(currentUser!!.capabilities)) {
+            if (isServerEOL(currentUser!!.serverVersion!!.major)) {
                 showServerEOLDialog()
                 return
             }
-            if (isUnifiedSearchAvailable(currentUser!!)) {
+            if (isUnifiedSearchAvailable(currentUser!!.capabilities!!.spreedCapability!!)) {
                 searchHelper = MessageSearchHelper(unifiedSearchRepository)
             }
             credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
@@ -423,7 +424,7 @@ class ConversationsListActivity :
     private fun loadUserAvatar(target: Target) {
         if (currentUser != null) {
             val url = ApiUtils.getUrlForAvatar(
-                currentUser!!.baseUrl,
+                currentUser!!.baseUrl!!,
                 currentUser!!.userId,
                 true
             )
@@ -433,7 +434,7 @@ class ConversationsListActivity :
             context.imageLoader.enqueue(
                 ImageRequest.Builder(context)
                     .data(url)
-                    .addHeader("Authorization", credentials)
+                    .addHeader("Authorization", credentials!!)
                     .placeholder(R.drawable.ic_user)
                     .transformations(CircleCropTransformation())
                     .crossfade(true)
@@ -698,7 +699,10 @@ class ConversationsListActivity :
         isRefreshing = true
         conversationItems = ArrayList()
         conversationItemsWithHeader = ArrayList()
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(
+            currentUser!!,
+            intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)
+        )
         val startNanoTime = System.nanoTime()
         Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime")
         roomsQueryDisposable = ncApi.getRooms(
@@ -868,11 +872,15 @@ class ConversationsListActivity :
     private fun fetchOpenConversations(apiVersion: Int) {
         searchableConversationItems.clear()
         searchableConversationItems.addAll(conversationItemsWithHeader)
-        if (hasSpreedFeatureCapability(currentUser, "listable-rooms")) {
+        if (hasSpreedFeatureCapability(
+                currentUser!!.capabilities!!.spreedCapability!!,
+                SpreedFeatures.LISTABLE_ROOMS
+            )
+        ) {
             val openConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
             openConversationsQueryDisposable = ncApi.getOpenConversations(
                 credentials,
-                ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl)
+                ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!)
             )
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
@@ -1087,7 +1095,7 @@ class ConversationsListActivity :
             clearMessageSearchResults()
             adapter!!.setFilter(filter)
             adapter!!.filterItems()
-            if (isUnifiedSearchAvailable(currentUser!!)) {
+            if (isUnifiedSearchAvailable(currentUser!!.capabilities!!.spreedCapability!!)) {
                 startMessageSearch(filter)
             }
         } else {
@@ -1173,7 +1181,11 @@ class ConversationsListActivity :
     private fun handleConversation(conversation: Conversation?) {
         selectedConversation = conversation
         if (selectedConversation != null) {
-            val hasChatPermission = ParticipantPermissions(currentUser!!, selectedConversation!!).hasChatPermission()
+            val hasChatPermission = ParticipantPermissions(
+                currentUser!!.capabilities!!.spreedCapability!!,
+                selectedConversation!!
+            )
+                .hasChatPermission()
             if (showShareToScreen) {
                 if (hasChatPermission &&
                     !isReadOnlyConversation(selectedConversation!!) &&
@@ -1197,7 +1209,10 @@ class ConversationsListActivity :
     }
 
     private fun shouldShowLobby(conversation: Conversation): Boolean {
-        val participantPermissions = ParticipantPermissions(currentUser!!, conversation)
+        val participantPermissions = ParticipantPermissions(
+            currentUser!!.capabilities?.spreedCapability!!,
+            conversation
+        )
         return conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
             !conversation.canModerate(currentUser!!) &&
             !participantPermissions.canIgnoreLobby()
@@ -1511,7 +1526,7 @@ class ConversationsListActivity :
                 .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ ->
                     val intent = Intent(context, WebViewLoginActivity::class.java)
                     val bundle = Bundle()
-                    bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl)
+                    bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl!!)
                     bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true)
                     intent.putExtras(bundle)
                     startActivity(intent)

+ 6 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

@@ -27,6 +27,7 @@ import com.nextcloud.talk.callnotification.viewmodel.CallNotificationViewModel
 import com.nextcloud.talk.chat.viewmodels.ChatViewModel
 import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel
 import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel
+import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
 import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
 import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel
 import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel
@@ -137,6 +138,11 @@ abstract class ViewModelModule {
     @ViewModelKey(CallNotificationViewModel::class)
     abstract fun callNotificationViewModel(viewModel: CallNotificationViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(ConversationInfoViewModel::class)
+    abstract fun conversationInfoViewModel(viewModel: ConversationInfoViewModel): ViewModel
+
     @Binds
     @IntoMap
     @ViewModelKey(ConversationInfoEditViewModel::class)

+ 3 - 3
app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt

@@ -36,7 +36,7 @@ object UserMapper {
                 entity.id,
                 entity.userId,
                 entity.username,
-                entity.baseUrl,
+                entity.baseUrl!!,
                 entity.token,
                 entity.displayName,
                 entity.pushConfigurationState,
@@ -52,8 +52,8 @@ object UserMapper {
 
     fun toEntity(model: User): UserEntity {
         val userEntity = when (val id = model.id) {
-            null -> UserEntity(userId = model.userId, username = model.username, baseUrl = model.baseUrl)
-            else -> UserEntity(id, model.userId, model.username, model.baseUrl)
+            null -> UserEntity(userId = model.userId, username = model.username, baseUrl = model.baseUrl!!)
+            else -> UserEntity(id, model.userId, model.username, model.baseUrl!!)
         }
         userEntity.apply {
             token = model.token

+ 1 - 1
app/src/main/java/com/nextcloud/talk/data/user/model/User.kt

@@ -45,7 +45,7 @@ data class User(
     var scheduledForDeletion: Boolean = FALSE
 ) : Parcelable {
 
-    fun getCredentials(): String = ApiUtils.getCredentials(username, token)
+    fun getCredentials(): String = ApiUtils.getCredentials(username, token)!!
 
     fun hasSpreedFeatureCapability(capabilityName: String): Boolean {
         return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false

+ 4 - 4
app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt

@@ -118,7 +118,7 @@ fun ImageView.loadUserAvatar(
     ignoreCache: Boolean
 ): io.reactivex.disposables.Disposable {
     val imageRequestUri = ApiUtils.getUrlForAvatar(
-        user.baseUrl,
+        user.baseUrl!!,
         avatarId,
         requestBigSize
     )
@@ -155,7 +155,7 @@ private fun ImageView.loadAvatarInternal(
             user?.let {
                 addHeader(
                     "Authorization",
-                    ApiUtils.getCredentials(user.username, user.token)
+                    ApiUtils.getCredentials(user.username, user.token)!!
                 )
             }
             transformations(CircleCropTransformation())
@@ -196,7 +196,7 @@ fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.D
     ) {
         requestBuilder.addHeader(
             "Authorization",
-            ApiUtils.getCredentials(user.username, user.token)
+            ApiUtils.getCredentials(user.username, user.token)!!
         )
     }
 
@@ -222,7 +222,7 @@ fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null):
     ) {
         requestBuilder.addHeader(
             "Authorization",
-            ApiUtils.getCredentials(user.username, user.token)
+            ApiUtils.getCredentials(user.username, user.token)!!
         )
     }
 

+ 6 - 6
app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt

@@ -29,29 +29,29 @@ class InvitationsRepositoryImpl(private val ncApi: NcApi) :
     InvitationsRepository {
 
     override fun fetchInvitations(user: User): Observable<InvitationsModel> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
 
         return ncApi.getInvitations(
             credentials,
-            ApiUtils.getUrlForInvitation(user.baseUrl)
+            ApiUtils.getUrlForInvitation(user.baseUrl!!)
         ).map { mapToInvitationsModel(user, it.ocs?.data!!) }
     }
 
     override fun acceptInvitation(user: User, invitation: Invitation): Observable<InvitationActionModel> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
 
         return ncApi.acceptInvitation(
             credentials,
-            ApiUtils.getUrlForInvitationAccept(user.baseUrl, invitation.id)
+            ApiUtils.getUrlForInvitationAccept(user.baseUrl!!, invitation.id)
         ).map { InvitationActionModel(ActionEnum.ACCEPT, it.ocs?.meta?.statusCode!!, invitation) }
     }
 
     override fun rejectInvitation(user: User, invitation: Invitation): Observable<InvitationActionModel> {
-        val credentials: String = ApiUtils.getCredentials(user.username, user.token)
+        val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
 
         return ncApi.rejectInvitation(
             credentials,
-            ApiUtils.getUrlForInvitationReject(user.baseUrl, invitation.id)
+            ApiUtils.getUrlForInvitationReject(user.baseUrl!!, invitation.id)
         ).map { InvitationActionModel(ActionEnum.REJECT, it.ocs?.meta?.statusCode!!, invitation) }
     }
 

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java

@@ -70,7 +70,7 @@ public class AddParticipantsToConversation extends Worker {
                 data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1))
                 .blockingGet();
 
-        int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
+        int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.API_V4, 1});
 
         String conversationToken = data.getString(BundleKeys.KEY_TOKEN);
         String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt

@@ -129,7 +129,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
 
             ncApi.searchContactsByPhoneNumber(
                 ApiUtils.getCredentials(currentUser.username, currentUser.token),
-                ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl),
+                ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl!!),
                 json.toRequestBody("application/json".toMediaTypeOrNull())
             )
                 .subscribeOn(Schedulers.io())

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java

@@ -80,7 +80,7 @@ public class DeleteConversationWorker extends Worker {
         User operationUser = userManager.getUserWithId(operationUserId).blockingGet();
 
         if (operationUser != null) {
-            int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[]{ApiUtils.APIv4, 1});
+            int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[]{ApiUtils.API_V4, 1});
 
             String credentials = ApiUtils.getCredentials(operationUser.getUsername(), operationUser.getToken());
             ncApi = retrofit

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.java

@@ -92,7 +92,7 @@ public class LeaveConversationWorker extends Worker {
                                                       EventStatus.EventType.CONVERSATION_UPDATE,
                                                       true);
 
-            int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[] {ApiUtils.APIv4, 1});
+            int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[] {ApiUtils.API_V4, 1});
 
             ncApi.removeSelfFromRoom(credentials, ApiUtils.getUrlForParticipantsSelf(apiVersion,
                                                                                      operationUser.getBaseUrl(),

+ 14 - 14
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt

@@ -248,7 +248,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
 
         val soundUri = getCallRingtoneUri(applicationContext, appPreferences)
         val notificationChannelId = NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
-        val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+        val uri = Uri.parse(signatureVerification.user!!.baseUrl!!)
         val baseUrl = uri.host
 
         val notification =
@@ -279,7 +279,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         credentials = ApiUtils.getCredentials(
             signatureVerification.user!!.username,
             signatureVerification.user!!.token
-        )
+        )!!
         ncApi = retrofit!!.newBuilder().client(
             okHttpClient!!.newBuilder().cookieJar(
                 JavaNetCookieJar(
@@ -338,7 +338,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         ncApi.getNcNotification(
             credentials,
             ApiUtils.getUrlForNcNotificationWithId(
-                user!!.baseUrl,
+                user!!.baseUrl!!,
                 (pushMessage.notificationId!!).toString()
             )
         )
@@ -451,7 +451,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             0
         }
         val pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag)
-        val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+        val uri = Uri.parse(signatureVerification.user!!.baseUrl!!)
         val baseUrl = uri.host
 
         var contentTitle: CharSequence? = ""
@@ -601,12 +601,12 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             val baseUrl = signatureVerification.user!!.baseUrl
             val avatarUrl = if ("user" == userType) {
                 ApiUtils.getUrlForAvatar(
-                    baseUrl,
+                    baseUrl!!,
                     notificationUser.id,
                     false
                 )
             } else {
-                ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.name, false)
+                ApiUtils.getUrlForGuestAvatar(baseUrl!!, notificationUser.name, false)
             }
             person.setIcon(loadAvatarSync(avatarUrl, context!!))
         }
@@ -840,8 +840,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
         var inCallOnDifferentDevice = false
 
         val apiVersion = ApiUtils.getConversationApiVersion(
-            signatureVerification.user,
-            intArrayOf(ApiUtils.APIv4, 1)
+            signatureVerification.user!!,
+            intArrayOf(ApiUtils.API_V4, 1)
         )
 
         var isCallNotificationVisible = true
@@ -850,8 +850,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
             credentials,
             ApiUtils.getUrlForCall(
                 apiVersion,
-                signatureVerification.user!!.baseUrl,
-                pushMessage.id
+                signatureVerification.user!!.baseUrl!!,
+                pushMessage.id!!
             )
         )
             .repeatWhen { completed ->
@@ -920,10 +920,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
 
         if (isOngoingCallNotificationVisible) {
             val apiVersion = ApiUtils.getConversationApiVersion(
-                signatureVerification.user,
+                signatureVerification.user!!,
                 intArrayOf(
-                    ApiUtils.APIv4,
-                    ApiUtils.APIv3,
+                    ApiUtils.API_V4,
+                    ApiUtils.API_V3,
                     1
                 )
             )
@@ -931,7 +931,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
                 credentials,
                 ApiUtils.getUrlForRoom(
                     apiVersion,
-                    signatureVerification.user?.baseUrl,
+                    signatureVerification.user?.baseUrl!!,
                     pushMessage.id
                 )
             )

+ 2 - 2
app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt

@@ -62,7 +62,7 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W
         for (filePath in filesArray) {
             ncApi.createRemoteShare(
                 credentials,
-                ApiUtils.getSharingUrl(baseUrl),
+                ApiUtils.getSharingUrl(baseUrl!!),
                 filePath,
                 roomToken,
                 "10",
@@ -87,7 +87,7 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W
 
         val operationsUser = userManager.getUserWithId(userId).blockingGet()
         baseUrl = operationsUser.baseUrl
-        credentials = ApiUtils.getCredentials(operationsUser.username, operationsUser.token)
+        credentials = ApiUtils.getCredentials(operationsUser.username, operationsUser.token)!!
     }
 
     companion object {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java

@@ -85,7 +85,7 @@ public class SignalingSettingsWorker extends Worker {
 
         for (User user : userEntityObjectList) {
 
-            int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[] {ApiUtils.APIv3, 2, 1});
+            int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[] {ApiUtils.API_V3, 2, 1});
 
             ncApi.getSignalingSettings(
                     ApiUtils.getCredentials(user.getUsername(), user.getToken()),

+ 4 - 2
app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

@@ -56,7 +56,7 @@ 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.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.preferences.AppPreferences
 import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -186,7 +186,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
     }
 
     private fun getRemotePath(currentUser: User): String {
-        var remotePath = CapabilitiesUtilNew.getAttachmentFolder(currentUser)!! + "/" + fileName
+        var remotePath = CapabilitiesUtil.getAttachmentFolder(
+            currentUser.capabilities!!.spreedCapability!!
+        ) + "/" + fileName
         remotePath = RemoteFileUtils.getNewPathIfFileExists(
             ncApi,
             currentUser,

+ 5 - 0
app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt

@@ -64,6 +64,7 @@ class GeocodingActivity :
     lateinit var okHttpClient: OkHttpClient
 
     lateinit var roomToken: String
+    private var chatApiVersion: Int = 1
     private var nominatimClient: TalkJsonNominatimClient? = null
 
     private var searchItem: MenuItem? = null
@@ -86,6 +87,7 @@ class GeocodingActivity :
         Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))
 
         roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
+        chatApiVersion = intent.getIntExtra(BundleKeys.KEY_CHAT_API_VERSION, 1)
 
         recyclerView = findViewById(R.id.geocoding_results)
         recyclerView.layoutManager = LinearLayoutManager(this)
@@ -130,6 +132,7 @@ class GeocodingActivity :
                 val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java)
                 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                 intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+                intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
                 intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult)
                 startActivity(intent)
             }
@@ -158,6 +161,7 @@ class GeocodingActivity :
                 val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java)
                 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                 intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+                intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
                 intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult)
                 startActivity(intent)
             }
@@ -217,6 +221,7 @@ class GeocodingActivity :
                     val intent = Intent(context, LocationPickerActivity::class.java)
                     intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                     intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken)
+                    intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
                     startActivity(intent)
                     return true
                 }

+ 5 - 2
app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt

@@ -58,6 +58,7 @@ import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CHAT_API_VERSION
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_GEOCODING_RESULT
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
 import fr.dudie.nominatim.client.TalkJsonNominatimClient
@@ -103,6 +104,7 @@ class LocationPickerActivity :
     var nominatimClient: TalkJsonNominatimClient? = null
 
     lateinit var roomToken: String
+    private var chatApiVersion: Int = 1
     var geocodingResult: GeocodingResult? = null
 
     var myLocation: GeoPoint = GeoPoint(COORDINATE_ZERO, COORDINATE_ZERO)
@@ -130,6 +132,7 @@ class LocationPickerActivity :
         NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
 
         roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
+        chatApiVersion = intent.getIntExtra(KEY_CHAT_API_VERSION, 1)
         geocodingResult = intent.getParcelableExtra(KEY_GEOCODING_RESULT)
 
         if (savedInstanceState != null) {
@@ -244,6 +247,7 @@ class LocationPickerActivity :
             val intent = Intent(this, GeocodingActivity::class.java)
             intent.putExtra(BundleKeys.KEY_GEOCODING_QUERY, query)
             intent.putExtra(KEY_ROOM_TOKEN, roomToken)
+            intent.putExtra(KEY_CHAT_API_VERSION, chatApiVersion)
             startActivity(intent)
         }
         return true
@@ -465,11 +469,10 @@ class LocationPickerActivity :
                 "\"longitude\":\"$selectedLon\",\"name\":\"$locationNameToShare\"}"
 
         val currentUser = userManager.currentUser.blockingGet()
-        val apiVersion = ApiUtils.getChatApiVersion(currentUser, intArrayOf(1))
 
         ncApi.sendLocation(
             ApiUtils.getCredentials(currentUser.username, currentUser.token),
-            ApiUtils.getUrlToSendLocation(apiVersion, currentUser.baseUrl, roomToken),
+            ApiUtils.getUrlToSendLocation(chatApiVersion, currentUser.baseUrl!!, roomToken),
             "geo-location",
             objectId,
             metaData

+ 1 - 1
app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt

@@ -27,5 +27,5 @@ import kotlinx.parcelize.Parcelize
 @Parcelize
 data class RetrofitBucket(
     var url: String? = null,
-    var queryMap: Map<String, String>? = null
+    var queryMap: MutableMap<String, String>? = null
 ) : Parcelable

+ 11 - 3
app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt

@@ -3,7 +3,7 @@ package com.nextcloud.talk.models.domain
 import com.nextcloud.talk.models.json.conversations.Conversation
 
 class ConversationModel(
-    var roomId: String?,
+    var roomId: String? = null,
     var token: String? = null,
     var name: String? = null,
     var displayName: String? = null,
@@ -42,7 +42,11 @@ class ConversationModel(
     var statusClearAt: Long? = 0,
     var callRecording: Int = 0,
     var avatarVersion: String? = null,
-    var hasCustomAvatar: Boolean? = null
+    var hasCustomAvatar: Boolean? = null,
+    var callStartTime: Long? = null,
+    var recordingConsentRequired: Int = 0,
+    var remoteServer: String? = null,
+    var remoteToken: String? = null
 ) {
 
     companion object {
@@ -95,7 +99,11 @@ class ConversationModel(
                 statusClearAt = conversation.statusClearAt,
                 callRecording = conversation.callRecording,
                 avatarVersion = conversation.avatarVersion,
-                hasCustomAvatar = conversation.hasCustomAvatar
+                hasCustomAvatar = conversation.hasCustomAvatar,
+                callStartTime = conversation.callStartTime,
+                recordingConsentRequired = conversation.recordingConsentRequired,
+                remoteServer = conversation.remoteServer,
+                remoteToken = conversation.remoteToken
             )
         }
     }

+ 45 - 0
app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt

@@ -0,0 +1,45 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.domain.converters
+
+import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter
+import com.nextcloud.talk.models.domain.NotificationLevel
+
+class DomainEnumNotificationLevelConverter : IntBasedTypeConverter<NotificationLevel>() {
+    override fun getFromInt(i: Int): NotificationLevel {
+        return when (i) {
+            0 -> NotificationLevel.DEFAULT
+            1 -> NotificationLevel.ALWAYS
+            2 -> NotificationLevel.MENTION
+            3 -> NotificationLevel.NEVER
+            else -> NotificationLevel.DEFAULT
+        }
+    }
+
+    override fun convertToInt(`object`: NotificationLevel): Int {
+        return when (`object`) {
+            NotificationLevel.DEFAULT -> 0
+            NotificationLevel.ALWAYS -> 1
+            NotificationLevel.MENTION -> 2
+            NotificationLevel.NEVER -> 3
+            else -> 0
+        }
+    }
+}

+ 42 - 0
app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt

@@ -0,0 +1,42 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.capabilities
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.generic.GenericMeta
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+data class RoomCapabilitiesOCS(
+    @JsonField(name = ["meta"])
+    var meta: GenericMeta?,
+    @JsonField(name = ["data"])
+    var data: SpreedCapability?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null)
+}

+ 37 - 0
app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.capabilities
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+data class RoomCapabilitiesOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: RoomCapabilitiesOCS? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 7 - 7
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt

@@ -37,7 +37,7 @@ import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
 import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.stfalcon.chatkit.commons.models.IUser
 import com.stfalcon.chatkit.commons.models.MessageContentType
 import kotlinx.parcelize.Parcelize
@@ -213,7 +213,7 @@ data class ChatMessage(
 
     @Suppress("ReturnCount")
     fun isLinkPreview(): Boolean {
-        if (CapabilitiesUtilNew.isLinkPreviewAvailable(activeUser!!)) {
+        if (CapabilitiesUtil.isLinkPreviewAvailable(activeUser!!)) {
             val regexStringFromServer = activeUser?.capabilities?.coreCapability?.referenceRegex
 
             val regexFromServer = regexStringFromServer?.toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
@@ -249,8 +249,8 @@ data class ChatMessage(
                     if (!isVoiceMessage) {
                         if (activeUser != null && activeUser!!.baseUrl != null) {
                             return ApiUtils.getUrlForFilePreviewWithFileId(
-                                activeUser!!.baseUrl,
-                                individualHashMap["id"],
+                                activeUser!!.baseUrl!!,
+                                individualHashMap["id"]!!,
                                 sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
                             )
                         } else {
@@ -413,11 +413,11 @@ data class ChatMessage(
                         null
                     }
                     actorType == "users" -> {
-                        ApiUtils.getUrlForAvatar(activeUser!!.baseUrl, actorId, true)
+                        ApiUtils.getUrlForAvatar(activeUser!!.baseUrl!!, actorId, true)
                     }
                     actorType == "bridged" -> {
                         ApiUtils.getUrlForAvatar(
-                            activeUser!!.baseUrl,
+                            activeUser!!.baseUrl!!,
                             "bridge-bot",
                             true
                         )
@@ -427,7 +427,7 @@ data class ChatMessage(
                         if (!TextUtils.isEmpty(actorDisplayName)) {
                             apiId = actorDisplayName
                         }
-                        ApiUtils.getUrlForGuestAvatar(activeUser!!.baseUrl, apiId, true)
+                        ApiUtils.getUrlForGuestAvatar(activeUser!!.baseUrl!!, apiId, true)
                     }
                 }
             }

+ 18 - 4
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt

@@ -38,8 +38,9 @@ import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter
 import com.nextcloud.talk.models.json.converters.EnumReadOnlyConversationConverter
 import com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter
 import com.nextcloud.talk.models.json.participants.Participant.ParticipantType
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ConversationUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import kotlinx.parcelize.Parcelize
 
 @Parcelize
@@ -160,7 +161,13 @@ data class Conversation(
     var callStartTime: Long? = null,
 
     @JsonField(name = ["recordingConsent"])
-    var recordingConsentRequired: Int = 0
+    var recordingConsentRequired: Int = 0,
+
+    @JsonField(name = ["remoteServer"])
+    var remoteServer: String? = null,
+
+    @JsonField(name = ["remoteToken"])
+    var remoteToken: String? = null
 
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
@@ -185,13 +192,20 @@ data class Conversation(
     @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")
+            CapabilitiesUtil.hasSpreedFeatureCapability(
+                conversationUser.capabilities?.spreedCapability!!,
+                SpreedFeatures.LOCKED_ONE_TO_ONE_ROOMS
+            )
     }
 
     @Deprecated("Use ConversationUtil")
     fun canModerate(conversationUser: User): Boolean {
         return isParticipantOwnerOrModerator &&
-            !isLockedOneToOne(conversationUser) &&
+            ConversationUtils.isLockedOneToOne(
+                ConversationModel.mapToConversationModel(this),
+                conversationUser
+                    .capabilities?.spreedCapability!!
+            ) &&
             type != ConversationType.FORMER_ONE_TO_ONE &&
             !ConversationUtils.isNoteToSelfConversation(ConversationModel.mapToConversationModel(this))
     }

+ 3 - 3
app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt

@@ -31,14 +31,14 @@ class OpenConversationsRepositoryImpl(private val ncApi: NcApi, currentUserProvi
     OpenConversationsRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
-    val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1))
+    val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
 
     override fun fetchConversations(): Observable<OpenConversationsModel> {
         return ncApi.getOpenConversations(
             credentials,
-            ApiUtils.getUrlForOpenConversations(apiVersion, currentUser.baseUrl)
+            ApiUtils.getUrlForOpenConversations(apiVersion, currentUser.baseUrl!!)
         ).map { mapToOpenConversationsModel(it.ocs?.data!!) }
     }
 

+ 5 - 5
app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt

@@ -37,7 +37,7 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
     PollRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
     override fun createPoll(
         roomToken: String,
@@ -49,7 +49,7 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
         return ncApi.createPoll(
             credentials,
             ApiUtils.getUrlForPoll(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken
             ),
             question,
@@ -63,7 +63,7 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
         return ncApi.getPoll(
             credentials,
             ApiUtils.getUrlForPoll(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken,
                 pollId
             )
@@ -74,7 +74,7 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
         return ncApi.votePoll(
             credentials,
             ApiUtils.getUrlForPoll(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken,
                 pollId
             ),
@@ -86,7 +86,7 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid
         return ncApi.closePoll(
             credentials,
             ApiUtils.getUrlForPoll(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken,
                 pollId
             )

+ 4 - 4
app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java

@@ -77,6 +77,7 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
     private Context context;
 
     private String roomToken;
+    private int chatApiVersion;
 
     private List<AbstractFlexibleItem> abstractFlexibleItemList = new ArrayList<>();
 
@@ -87,10 +88,11 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
         currentUser = userManager.getCurrentUser().blockingGet();
     }
 
-    public MentionAutocompletePresenter(Context context, String roomToken) {
+    public MentionAutocompletePresenter(Context context, String roomToken, int chatApiVersion) {
         super(context);
         this.roomToken = roomToken;
         this.context = context;
+        this.chatApiVersion = chatApiVersion;
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
         currentUser = userManager.getCurrentUser().blockingGet();
     }
@@ -120,8 +122,6 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
             queryString = "";
         }
 
-        int apiVersion = ApiUtils.getChatApiVersion(currentUser, new int[] {1});
-
         adapter.setFilter(queryString);
 
         Map<String, String> queryMap = new HashMap<>();
@@ -129,7 +129,7 @@ public class MentionAutocompletePresenter extends RecyclerViewPresenter<Mention>
 
         ncApi.getMentionAutocompleteSuggestions(
                 ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                ApiUtils.getUrlForMentionSuggestions(apiVersion, currentUser.getBaseUrl(), roomToken),
+                ApiUtils.getUrlForMentionSuggestions(chatApiVersion, currentUser.getBaseUrl(), roomToken),
                 queryString, 5, queryMap)
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())

+ 16 - 11
app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt

@@ -66,12 +66,13 @@ import com.nextcloud.talk.ui.dialog.ScopeDialog
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.Mimetype.IMAGE_JPG
 import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
 import com.nextcloud.talk.utils.PickImage
 import com.nextcloud.talk.utils.PickImage.Companion.REQUEST_PERMISSION_CAMERA
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
@@ -127,7 +128,7 @@ class ProfileActivity : BaseActivity() {
         binding.avatarDelete.setOnClickListener {
             ncApi.deleteAvatar(
                 credentials,
-                ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl)
+                ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl!!)
             )
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
@@ -154,7 +155,7 @@ class ProfileActivity : BaseActivity() {
                 })
         }
         binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") }
-        ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl))
+        ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!))
             .retry(DEFAULT_RETRIES)
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
@@ -226,13 +227,17 @@ class ProfileActivity : BaseActivity() {
                 item.icon = ContextCompat.getDrawable(this, R.drawable.ic_check)
                 binding.emptyList.root.visibility = View.GONE
                 binding.userinfoList.visibility = View.VISIBLE
-                if (CapabilitiesUtilNew.isAvatarEndpointAvailable(currentUser!!)) {
+                if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                        currentUser!!.capabilities!!.spreedCapability!!,
+                        SpreedFeatures.TEMP_USER_AVATAR_API
+                    )
+                ) {
                     // TODO later avatar can also be checked via user fields, for now it is in Talk capability
                     binding.avatarButtons.visibility = View.VISIBLE
                 }
                 ncApi.getEditableUserProfileFields(
                     ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-                    ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
+                    ApiUtils.getUrlForUserFields(currentUser!!.baseUrl!!)
                 )
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
@@ -292,7 +297,7 @@ class ProfileActivity : BaseActivity() {
 
     private fun showUserProfile() {
         if (currentUser!!.baseUrl != null) {
-            binding.userinfoBaseurl.text = Uri.parse(currentUser!!.baseUrl).host
+            binding.userinfoBaseurl.text = Uri.parse(currentUser!!.baseUrl!!).host
         }
         DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, false)
         if (!TextUtils.isEmpty(userInfo?.displayName)) {
@@ -327,10 +332,10 @@ class ProfileActivity : BaseActivity() {
         }
 
         // show edit button
-        if (CapabilitiesUtilNew.canEditScopes(currentUser!!)) {
+        if (CapabilitiesUtil.canEditScopes(currentUser!!)) {
             ncApi.getEditableUserProfileFields(
                 ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-                ApiUtils.getUrlForUserFields(currentUser!!.baseUrl)
+                ApiUtils.getUrlForUserFields(currentUser!!.baseUrl!!)
             )
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
@@ -438,7 +443,7 @@ class ProfileActivity : BaseActivity() {
                 val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
                 ncApi.setUserData(
                     credentials,
-                    ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+                    ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!),
                     item.field.fieldName,
                     item.text
                 )
@@ -535,7 +540,7 @@ class ProfileActivity : BaseActivity() {
         // upload file
         ncApi.uploadAvatar(
             ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-            ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl),
+            ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl!!),
             filePart
         )
             .subscribeOn(Schedulers.io())
@@ -569,7 +574,7 @@ class ProfileActivity : BaseActivity() {
         val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
         ncApi.setUserData(
             credentials,
-            ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+            ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!),
             item.field.scopeName,
             item.scope!!.name
         )

+ 1 - 1
app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt

@@ -31,7 +31,7 @@ class RequestAssistanceRepositoryImpl(private val ncApi: NcApi, currentUserProvi
     RequestAssistanceRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
     var apiVersion = 1
 

+ 3 - 3
app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt

@@ -91,8 +91,8 @@ class DirectReplyReceiver : BroadcastReceiver() {
 
     private fun sendDirectReply() {
         val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
-        val apiVersion = ApiUtils.getChatApiVersion(currentUser, intArrayOf(1))
-        val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl, roomToken)
+        val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1))
+        val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl!!, roomToken!!)
 
         ncApi.sendChatMessage(credentials, url, replyMessage, currentUser.displayName, null, false)
             ?.subscribeOn(Schedulers.io())
@@ -153,7 +153,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
 
         // Add reply
         Single.fromCallable {
-            val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl, currentUser.userId, false)
+            val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl!!, currentUser.userId, false)
             val me = Person.Builder()
                 .setName(currentUser.displayName)
                 .setIcon(NotificationUtils.loadAvatarSync(avatarUrl, context))

+ 3 - 3
app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt

@@ -80,11 +80,11 @@ class MarkAsReadReceiver : BroadcastReceiver() {
 
     private fun markAsRead() {
         val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
-        val apiVersion = ApiUtils.getChatApiVersion(currentUser, intArrayOf(1))
+        val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1))
         val url = ApiUtils.getUrlForChatReadMarker(
             apiVersion,
-            currentUser.baseUrl,
-            roomToken
+            currentUser.baseUrl!!,
+            roomToken!!
         )
 
         ncApi.setChatReadMarker(credentials, url, messageId)

+ 1 - 1
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt

@@ -94,7 +94,7 @@ class RemoteFileBrowserItemsListViewHolder(
 
         if (item.hasPreview) {
             val path = ApiUtils.getUrlForFilePreviewWithRemotePath(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 item.path,
                 fileIcon.context.resources.getDimensionPixelSize(R.dimen.small_item_height)
             )

+ 3 - 3
app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt

@@ -33,7 +33,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
     CallRecordingRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
     var apiVersion = 1
 
@@ -42,7 +42,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
             credentials,
             ApiUtils.getUrlForRecording(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken
             ),
             1
@@ -54,7 +54,7 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
             credentials,
             ApiUtils.getUrlForRecording(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken
             )
         ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) }

+ 3 - 3
app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt

@@ -38,12 +38,12 @@ class ConversationsRepositoryImpl(private val api: NcApi, private val userProvid
         get() = userProvider.currentUser.blockingGet()
 
     private val credentials: String
-        get() = ApiUtils.getCredentials(user.username, user.token)
+        get() = ApiUtils.getCredentials(user.username, user.token)!!
 
     override fun allowGuests(token: String, allow: Boolean): Observable<AllowGuestsResult> {
         val url = ApiUtils.getUrlForRoomPublic(
             apiVersion(),
-            user.baseUrl,
+            user.baseUrl!!,
             token
         )
 
@@ -100,7 +100,7 @@ class ConversationsRepositoryImpl(private val api: NcApi, private val userProvid
     }
 
     private fun apiVersion(): Int {
-        return ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4))
+        return ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4))
     }
 
     companion object {

+ 3 - 3
app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt

@@ -34,13 +34,13 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur
     ReactionsRepository {
 
     val currentUser: User = currentUserProvider.currentUser.blockingGet()
-    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+    val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
 
     override fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable<ReactionAddedModel> {
         return ncApi.sendReaction(
             credentials,
             ApiUtils.getUrlForMessageReaction(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken,
                 message.id
             ),
@@ -56,7 +56,7 @@ class ReactionsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: Cur
         return ncApi.deleteReaction(
             credentials,
             ApiUtils.getUrlForMessageReaction(
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 roomToken,
                 message.id
             ),

+ 1 - 1
app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt

@@ -37,7 +37,7 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid
         get() = userProvider.currentUser.blockingGet()
 
     private val credentials: String
-        get() = ApiUtils.getCredentials(user.username, user.token)
+        get() = ApiUtils.getCredentials(user.username, user.token)!!
 
     override fun searchMessages(
         searchTerm: String,

+ 20 - 15
app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt

@@ -90,6 +90,7 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
 import com.nextcloud.talk.profile.ProfileActivity
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.DisplayUtils
 import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment
@@ -97,7 +98,7 @@ import com.nextcloud.talk.utils.NotificationUtils
 import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
 import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
 import com.nextcloud.talk.utils.SecurityUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
 import com.nextcloud.talk.utils.power.PowerManagerUtils
@@ -266,7 +267,11 @@ class SettingsActivity : BaseActivity() {
     }
 
     private fun setupPhoneBookIntegration() {
-        if (CapabilitiesUtilNew.isPhoneBookIntegrationAvailable(currentUser!!)) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser?.capabilities?.spreedCapability!!,
+                SpreedFeatures.PHONEBOOK_SEARCH
+            )
+        ) {
             binding.settingsPhoneBookIntegration.visibility = View.VISIBLE
         } else {
             binding.settingsPhoneBookIntegration.visibility = View.GONE
@@ -507,7 +512,7 @@ class SettingsActivity : BaseActivity() {
         var port = -1
         val uri: URI
         try {
-            uri = URI(currentUser!!.baseUrl)
+            uri = URI(currentUser!!.baseUrl!!)
             host = uri.host
             port = uri.port
             Log.d(TAG, "uri is $uri")
@@ -823,7 +828,7 @@ class SettingsActivity : BaseActivity() {
     private fun setupProfileQueryDisposable() {
         profileQueryDisposable = ncApi.getUserProfile(
             credentials,
-            ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl)
+            ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!)
         )
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
@@ -854,7 +859,7 @@ class SettingsActivity : BaseActivity() {
 
     private fun setupServerAgeWarning() {
         when {
-            CapabilitiesUtilNew.isServerEOL(currentUser!!.capabilities) -> {
+            CapabilitiesUtil.isServerEOL(currentUser!!.serverVersion!!.major) -> {
                 binding.serverAgeWarningText.setTextColor(ContextCompat.getColor((context), R.color.nc_darkRed))
                 binding.serverAgeWarningText.setText(R.string.nc_settings_server_eol)
                 binding.serverAgeWarningIcon.setColorFilter(
@@ -863,7 +868,7 @@ class SettingsActivity : BaseActivity() {
                 )
             }
 
-            CapabilitiesUtilNew.isServerAlmostEOL(currentUser!!) -> {
+            CapabilitiesUtil.isServerAlmostEOL(currentUser!!.serverVersion!!.major) -> {
                 binding.serverAgeWarningText.setTextColor(
                     ContextCompat.getColor((context), R.color.nc_darkYellow)
                 )
@@ -889,8 +894,8 @@ class SettingsActivity : BaseActivity() {
             binding.settingsIncognitoKeyboardSwitch.visibility = View.GONE
         }
 
-        if (CapabilitiesUtilNew.isReadStatusAvailable(currentUser!!)) {
-            binding.settingsReadPrivacySwitch.isChecked = !CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!)
+        if (CapabilitiesUtil.isReadStatusAvailable(currentUser!!.capabilities!!.spreedCapability!!)) {
+            binding.settingsReadPrivacySwitch.isChecked = !CapabilitiesUtil.isReadStatusPrivate(currentUser!!)
         } else {
             binding.settingsReadPrivacy.visibility = View.GONE
         }
@@ -954,10 +959,10 @@ class SettingsActivity : BaseActivity() {
     private fun setupTypingStatusSetting() {
         if (currentUser!!.externalSignalingServer?.externalSignalingServer?.isNotEmpty() == true) {
             binding.settingsTypingStatusOnlyWithHpb.visibility = View.GONE
-            Log.i(TAG, "Typing Status Available: ${CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)}")
+            Log.i(TAG, "Typing Status Available: ${CapabilitiesUtil.isTypingStatusAvailable(currentUser!!)}")
 
-            if (CapabilitiesUtilNew.isTypingStatusAvailable(currentUser!!)) {
-                binding.settingsTypingStatusSwitch.isChecked = !CapabilitiesUtilNew.isTypingStatusPrivate(currentUser!!)
+            if (CapabilitiesUtil.isTypingStatusAvailable(currentUser!!)) {
+                binding.settingsTypingStatusSwitch.isChecked = !CapabilitiesUtil.isTypingStatusPrivate(currentUser!!)
             } else {
                 binding.settingsTypingStatus.visibility = View.GONE
             }
@@ -1209,7 +1214,7 @@ class SettingsActivity : BaseActivity() {
     private fun checkForPhoneNumber() {
         ncApi.getUserData(
             ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-            ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl)
+            ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!)
         ).subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<UserProfileOverall> {
@@ -1294,7 +1299,7 @@ class SettingsActivity : BaseActivity() {
         val phoneNumber = textInputLayout.editText!!.text.toString()
         ncApi.setUserData(
             ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-            ApiUtils.getUrlForUserData(currentUser!!.baseUrl, currentUser!!.userId),
+            ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!),
             "phone",
             phoneNumber
         ).subscribeOn(Schedulers.io())
@@ -1349,7 +1354,7 @@ class SettingsActivity : BaseActivity() {
                     val json = "{\"key\": \"read_status_privacy\", \"value\" : $booleanValue}"
                     ncApi.setReadStatusPrivacy(
                         ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-                        ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl),
+                        ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl!!),
                         json.toRequestBody("application/json".toMediaTypeOrNull())
                     )
                         .subscribeOn(Schedulers.io())
@@ -1387,7 +1392,7 @@ class SettingsActivity : BaseActivity() {
                     val json = "{\"key\": \"typing_privacy\", \"value\" : $booleanValue}"
                     ncApi.setTypingStatusPrivacy(
                         ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token),
-                        ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl),
+                        ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl!!),
                         json.toRequestBody("application/json".toMediaTypeOrNull())
                     )
                         .subscribeOn(Schedulers.io())

+ 3 - 3
app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt

@@ -105,7 +105,7 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr
                         fileParameters["link"]!!,
                         fileParameters["mimetype"]!!,
                         previewAvailable,
-                        previewLink(fileParameters["id"], parameters.baseUrl)
+                        previewLink(fileParameters["id"], parameters.baseUrl!!)
                     )
                 } else if (it.value.messageParameters?.containsKey("object") == true) {
                     val objectParameters = it.value.messageParameters!!["object"]!!
@@ -184,7 +184,7 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr
 
         return ncApi.getSharedItemsOverview(
             credentials,
-            ApiUtils.getUrlForChatSharedItemsOverview(1, parameters.baseUrl, parameters.roomToken),
+            ApiUtils.getUrlForChatSharedItemsOverview(1, parameters.baseUrl!!, parameters.roomToken),
             1
         ).map {
             val types = mutableSetOf<SharedItemType>()
@@ -206,7 +206,7 @@ class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, pr
     private fun previewLink(fileId: String?, baseUrl: String): String {
         return ApiUtils.getUrlForFilePreviewWithFileId(
             baseUrl,
-            fileId,
+            fileId!!,
             sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size)
         )
     }

+ 4 - 4
app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt

@@ -37,8 +37,8 @@ class TranslateViewModel @Inject constructor(
 
     fun translateMessage(toLanguage: String, fromLanguage: String?, text: String) {
         val currentUser: User = userManager.currentUser.blockingGet()
-        val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
-        val url: String = ApiUtils.getUrlForTranslation(currentUser.baseUrl)
+        val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
+        val url: String = ApiUtils.getUrlForTranslation(currentUser.baseUrl!!)
         val calculatedFromLanguage =
             if (fromLanguage == null || fromLanguage == "") {
                 null
@@ -60,8 +60,8 @@ class TranslateViewModel @Inject constructor(
 
     fun getLanguages() {
         val currentUser: User = userManager.currentUser.blockingGet()
-        val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
-        val url: String = ApiUtils.getUrlForLanguages(currentUser.baseUrl)
+        val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
+        val url: String = ApiUtils.getUrlForLanguages(currentUser.baseUrl!!)
         Log.d(TAG, "URL is: $url")
         repository.getLanguages(authorization, url)
             .subscribeOn(Schedulers.io())

+ 3 - 3
app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt

@@ -57,7 +57,7 @@ class ProfileBottomSheet(val ncApi: NcApi, val userModel: User, val viewThemeUti
     fun showFor(user: String, context: Context) {
         ncApi.hoverCard(
             ApiUtils.getCredentials(userModel.username, userModel.token),
-            ApiUtils.getUrlForHoverCard(userModel.baseUrl, user)
+            ApiUtils.getUrlForHoverCard(userModel.baseUrl!!, user)
         ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
             .subscribe(object : Observer<HoverCardOverall> {
                 override fun onSubscribe(d: Disposable) {
@@ -121,10 +121,10 @@ class ProfileBottomSheet(val ncApi: NcApi, val userModel: User, val viewThemeUti
 
     private fun talkTo(userId: String, context: Context) {
         val apiVersion =
-            ApiUtils.getConversationApiVersion(userModel, intArrayOf(ApiUtils.APIv4, 1))
+            ApiUtils.getConversationApiVersion(userModel, intArrayOf(ApiUtils.API_V4, 1))
         val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
             apiVersion,
-            userModel.baseUrl,
+            userModel.baseUrl!!,
             "1",
             null,
             userId,

+ 7 - 6
app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt

@@ -35,7 +35,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.chat.ChatActivity
 import com.nextcloud.talk.databinding.DialogAttachmentBinding
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.SpreedFeatures
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
@@ -61,7 +62,7 @@ class AttachmentDialog(val activity: Activity, var chatActivity: ChatActivity) :
     }
 
     private fun initItemsStrings() {
-        var serverName = CapabilitiesUtilNew.getServerName(chatActivity.conversationUser)
+        var serverName = CapabilitiesUtil.getServerName(chatActivity.conversationUser)
         dialogAttachmentBinding.txtAttachFileFromCloud.text = chatActivity.resources?.let {
             if (serverName.isNullOrEmpty()) {
                 serverName = it.getString(R.string.nc_server_product_name)
@@ -71,15 +72,15 @@ class AttachmentDialog(val activity: Activity, var chatActivity: ChatActivity) :
     }
 
     private fun initItemsVisibility() {
-        if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(
-                chatActivity.conversationUser,
-                "geo-location-sharing"
+        if (!CapabilitiesUtil.hasSpreedFeatureCapability(
+                chatActivity.spreedCapabilities,
+                SpreedFeatures.GEO_LOCATION_SHARING
             )
         ) {
             dialogAttachmentBinding.menuShareLocation.visibility = View.GONE
         }
 
-        if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(chatActivity.conversationUser, "talk-polls") ||
+        if (!CapabilitiesUtil.hasSpreedFeatureCapability(chatActivity.spreedCapabilities, SpreedFeatures.TALK_POLLS) ||
             chatActivity.isOneToOneConversation()
         ) {
             dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE

+ 2 - 2
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java

@@ -54,7 +54,7 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils;
 import com.nextcloud.talk.users.UserManager;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
+import com.nextcloud.talk.utils.CapabilitiesUtil;
 
 import java.net.CookieManager;
 import java.util.ArrayList;
@@ -262,7 +262,7 @@ public class ChooseAccountDialogFragment extends DialogFragment {
     private void loadCurrentStatus(User user) {
         String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());
 
-        if (CapabilitiesUtilNew.isUserStatusAvailable(userManager.getCurrentUser().blockingGet())) {
+        if (CapabilitiesUtil.isUserStatusAvailable(userManager.getCurrentUser().blockingGet())) {
             binding.statusView.setVisibility(View.VISIBLE);
 
             ncApi.status(credentials, ApiUtils.getUrlForStatus(user.getBaseUrl())).

+ 1 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt

@@ -89,7 +89,7 @@ class ChooseAccountShareToDialogFragment : DialogFragment() {
         if (user != null) {
             binding!!.currentAccount.userName.text = user.displayName
             binding!!.currentAccount.ticker.visibility = View.GONE
-            binding!!.currentAccount.account.text = Uri.parse(user.baseUrl).host
+            binding!!.currentAccount.account.text = Uri.parse(user.baseUrl!!).host
             viewThemeUtils!!.platform.colorImageView(binding!!.currentAccount.accountMenu, ColorRole.PRIMARY)
             if (user.baseUrl != null &&
                 (user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://"))

+ 25 - 14
app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt

@@ -46,7 +46,7 @@ import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
 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.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
@@ -86,7 +86,7 @@ class ConversationsListBottomDialog(
         initItemsVisibility()
         initClickListeners()
 
-        credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
+        credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
     }
 
     override fun onStart() {
@@ -105,7 +105,10 @@ class ConversationsListBottomDialog(
     }
 
     private fun initItemsVisibility() {
-        val hasFavoritesCapability = CapabilitiesUtilNew.hasSpreedFeatureCapability(currentUser, "favorites")
+        val hasFavoritesCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
+            currentUser.capabilities?.spreedCapability!!,
+            "favorites"
+        )
         val canModerate = conversation.canModerate(currentUser)
 
         binding.conversationRemoveFromFavorites.visibility = setVisibleIf(
@@ -116,11 +119,19 @@ class ConversationsListBottomDialog(
         )
 
         binding.conversationMarkAsRead.visibility = setVisibleIf(
-            conversation.unreadMessages > 0 && CapabilitiesUtilNew.canSetChatReadMarker(currentUser)
+            conversation.unreadMessages > 0 && CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser
+                    .capabilities?.spreedCapability!!,
+                "chat-read-marker"
+            )
         )
 
         binding.conversationMarkAsUnread.visibility = setVisibleIf(
-            conversation.unreadMessages <= 0 && CapabilitiesUtilNew.canMarkRoomAsUnread(currentUser)
+            conversation.unreadMessages <= 0 && CapabilitiesUtil.hasSpreedFeatureCapability(
+                currentUser
+                    .capabilities?.spreedCapability!!,
+                "chat-unread"
+            )
         )
 
         binding.conversationOperationRename.visibility = setVisibleIf(
@@ -178,12 +189,12 @@ class ConversationsListBottomDialog(
     }
 
     private fun addConversationToFavorites() {
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
         ncApi.addConversationToFavorites(
             credentials,
             ApiUtils.getUrlForRoomFavorite(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 conversation.token
             )
         )
@@ -218,12 +229,12 @@ class ConversationsListBottomDialog(
     }
 
     private fun removeConversationFromFavorites() {
-        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv1))
+        val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
         ncApi.removeConversationFromFavorites(
             credentials,
             ApiUtils.getUrlForRoomFavorite(
                 apiVersion,
-                currentUser.baseUrl,
+                currentUser.baseUrl!!,
                 conversation.token
             )
         )
@@ -262,8 +273,8 @@ class ConversationsListBottomDialog(
             credentials,
             ApiUtils.getUrlForChatReadMarker(
                 chatApiVersion(),
-                currentUser.baseUrl,
-                conversation.token
+                currentUser.baseUrl!!,
+                conversation.token!!
             )
         )
             .subscribeOn(Schedulers.io())
@@ -301,8 +312,8 @@ class ConversationsListBottomDialog(
             credentials,
             ApiUtils.getUrlForChatReadMarker(
                 chatApiVersion(),
-                currentUser.baseUrl,
-                conversation.token
+                currentUser.baseUrl!!,
+                conversation.token!!
             ),
             conversation.lastMessage!!.jsonMessageId
         )
@@ -396,7 +407,7 @@ class ConversationsListBottomDialog(
     }
 
     private fun chatApiVersion(): Int {
-        return ApiUtils.getChatApiVersion(currentUser, intArrayOf(ApiUtils.APIv1))
+        return ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(ApiUtils.API_V1))
     }
 
     companion object {

+ 14 - 6
app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimePickerFragment.kt

@@ -50,7 +50,8 @@ import javax.inject.Inject
 class DateTimePickerFragment(
     token: String,
     id: String,
-    chatViewModel: ChatViewModel
+    chatViewModel: ChatViewModel,
+    private val chatApiVersion: Int
 ) : DialogFragment() {
     lateinit var binding: DialogDateTimePickerBinding
     private var dialogView: View? = null
@@ -144,7 +145,7 @@ class DateTimePickerFragment(
     }
 
     private fun getReminder() {
-        viewModel.getReminder(userManager.currentUser.blockingGet(), roomToken, messageId)
+        viewModel.getReminder(userManager.currentUser.blockingGet(), roomToken, messageId, chatApiVersion)
     }
 
     private fun showDelete(value: Boolean) {
@@ -221,12 +222,18 @@ class DateTimePickerFragment(
         binding.buttonClose.setOnClickListener { dismiss() }
         binding.buttonSet.setOnClickListener {
             currentTimeStamp?.let { time ->
-                viewModel.setReminder(userManager.currentUser.blockingGet(), roomToken, messageId, time.toInt())
+                viewModel.setReminder(
+                    userManager.currentUser.blockingGet(),
+                    roomToken,
+                    messageId,
+                    time.toInt(),
+                    chatApiVersion
+                )
             }
             dismiss()
         }
         binding.buttonDelete.setOnClickListener {
-            viewModel.deleteReminder(userManager.currentUser.blockingGet(), roomToken, messageId)
+            viewModel.deleteReminder(userManager.currentUser.blockingGet(), roomToken, messageId, chatApiVersion)
         }
     }
 
@@ -299,11 +306,12 @@ class DateTimePickerFragment(
         private const val HOUR_SIX_PM = 18
 
         @JvmStatic
-        fun newInstance(token: String, id: String, chatViewModel: ChatViewModel) =
+        fun newInstance(token: String, id: String, chatViewModel: ChatViewModel, chatApiVersion: Int) =
             DateTimePickerFragment(
                 token,
                 id,
-                chatViewModel
+                chatViewModel,
+                chatApiVersion
             )
     }
 }

+ 16 - 9
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -46,14 +46,17 @@ 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.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.repositories.reactions.ReactionsRepository
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
 import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.SpreedFeatures
 import com.nextcloud.talk.utils.ConversationUtils
 import com.nextcloud.talk.utils.DateConstants
 import com.nextcloud.talk.utils.DateUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
 import com.vanniktech.emoji.EmojiPopup
 import com.vanniktech.emoji.EmojiTextView
 import com.vanniktech.emoji.installDisableKeyboardInput
@@ -72,7 +75,8 @@ class MessageActionsDialog(
     private val user: User?,
     private val currentConversation: ConversationModel?,
     private val showMessageDeletionButton: Boolean,
-    private val hasChatPermission: Boolean
+    private val hasChatPermission: Boolean,
+    private val spreedCapabilities: SpreedCapability
 ) : BottomSheetDialog(chatActivity) {
 
     @Inject
@@ -100,8 +104,8 @@ class MessageActionsDialog(
 
     private val isUserAllowedToEdit = chatActivity.userAllowedByPrivilages(message)
 
-    private val isMessageEditable = CapabilitiesUtilNew.hasSpreedFeatureCapability(
-        user,
+    private val isMessageEditable = CapabilitiesUtil.hasSpreedFeatureCapability(
+        spreedCapabilities,
         "edit-messages"
     ) && messageHasRegularText && !isOlderThanTwentyFourHours && isUserAllowedToEdit
 
@@ -116,9 +120,9 @@ class MessageActionsDialog(
         viewThemeUtils.platform.themeDialog(dialogMessageActionsBinding.root)
         initEmojiBar(hasChatPermission)
         initMenuItemCopy(!message.isDeleted)
-        val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.APIv4, ApiUtils.APIv3, 1))
+        val apiVersion = ApiUtils.getConversationApiVersion(user!!, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
         chatActivity.chatViewModel.checkForNoteToSelf(
-            ApiUtils.getCredentials(user!!.username, user.token),
+            ApiUtils.getCredentials(user!!.username, user.token)!!,
             ApiUtils.getUrlForRooms(
                 apiVersion,
                 user.baseUrl
@@ -144,7 +148,7 @@ class MessageActionsDialog(
         initMenuItemTranslate(
             !message.isDeleted &&
                 ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
-                CapabilitiesUtilNew.isTranslationsSupported(user)
+                CapabilitiesUtil.isTranslationsSupported(spreedCapabilities)
         )
         initMenuEditorDetails(message.lastEditTimestamp != 0L && !message.isDeleted)
         initMenuReplyToMessage(message.replyable && hasChatPermission)
@@ -160,7 +164,10 @@ class MessageActionsDialog(
             ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
                 !(message.isDeletedCommentMessage || message.isDeleted)
         )
-        initMenuRemindMessage(!message.isDeleted && CapabilitiesUtilNew.isRemindSupported(user))
+        initMenuRemindMessage(
+            !message.isDeleted && CapabilitiesUtil.hasSpreedFeatureCapability
+                (spreedCapabilities, "remind-me-later")
+        )
         initMenuMarkAsUnread(
             message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
                 ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType()
@@ -242,7 +249,7 @@ class MessageActionsDialog(
     }
 
     private fun initEmojiBar(hasChatPermission: Boolean) {
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "reactions") &&
+        if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACTIONS) &&
             isPermitted(hasChatPermission) &&
             isReactableMessageType(message)
         ) {

+ 3 - 3
app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt

@@ -35,7 +35,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding
 import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel
 import com.nextcloud.talk.ui.theme.ViewThemeUtils
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.viewmodels.CallRecordingViewModel
 import com.vanniktech.emoji.EmojiTextView
 import javax.inject.Inject
@@ -72,7 +72,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
     }
 
     private fun initItemsVisibility() {
-        if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) {
+        if (CapabilitiesUtil.isCallReactionsSupported(callActivity.conversationUser)) {
             binding.callEmojiBar.visibility = View.VISIBLE
         } else {
             binding.callEmojiBar.visibility = View.GONE
@@ -102,7 +102,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
     }
 
     private fun initEmojiBar() {
-        if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) {
+        if (CapabilitiesUtil.isCallReactionsSupported(callActivity.conversationUser)) {
             binding.advancedCallOptionsTitle.visibility = View.GONE
 
             val capabilities = callActivity.conversationUser?.capabilities

+ 6 - 6
app/src/main/java/com/nextcloud/talk/ui/dialog/SetStatusDialogFragment.kt

@@ -128,8 +128,8 @@ class SetStatusDialogFragment :
             currentUser = currentUserProvider?.currentUser?.blockingGet()
             currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
 
-            credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
-            ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl))
+            credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)!!
+            ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl!!))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(object : Observer<ResponseBody> {
@@ -369,7 +369,7 @@ class SetStatusDialogFragment :
 
     private fun clearStatus() {
         val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)
-        ncApi.statusDeleteMessage(credentials, ApiUtils.getUrlForStatusMessage(currentUser?.baseUrl))
+        ncApi.statusDeleteMessage(credentials, ApiUtils.getUrlForStatusMessage(currentUser?.baseUrl!!))
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<GenericOverall> {
                 override fun onSubscribe(d: Disposable) {
@@ -393,7 +393,7 @@ class SetStatusDialogFragment :
     private fun setStatus(statusType: StatusType) {
         visualizeStatus(statusType)
 
-        ncApi.setStatusType(credentials, ApiUtils.getUrlForSetStatusType(currentUser?.baseUrl), statusType.string)
+        ncApi.setStatusType(credentials, ApiUtils.getUrlForSetStatusType(currentUser?.baseUrl!!), statusType.string)
             .subscribeOn(
                 Schedulers
                     .io()
@@ -468,7 +468,7 @@ class SetStatusDialogFragment :
         ) {
             ncApi.setCustomStatusMessage(
                 credentials,
-                ApiUtils.getUrlForSetCustomStatus(currentUser?.baseUrl),
+                ApiUtils.getUrlForSetCustomStatus(currentUser?.baseUrl!!),
                 statusIcon,
                 inputText,
                 clearAt
@@ -499,7 +499,7 @@ class SetStatusDialogFragment :
 
             ncApi.setPredefinedStatusMessage(
                 credentials,
-                ApiUtils.getUrlForSetPredefinedStatus(currentUser?.baseUrl),
+                ApiUtils.getUrlForSetPredefinedStatus(currentUser?.baseUrl!!),
                 selectedPredefinedStatus!!.id,
                 if (clearAt == -1L) null else clearAt
             )

+ 2 - 2
app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt

@@ -154,7 +154,7 @@ class ShowReactionsDialog(
         ncApi.getReactions(
             credentials,
             ApiUtils.getUrlForMessageReaction(
-                user?.baseUrl,
+                user?.baseUrl!!,
                 roomToken,
                 chatMessage.id
             ),
@@ -209,7 +209,7 @@ class ShowReactionsDialog(
         ncApi.deleteReaction(
             credentials,
             ApiUtils.getUrlForMessageReaction(
-                user?.baseUrl,
+                user?.baseUrl!!,
                 roomToken,
                 message.id
             ),

+ 4 - 4
app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt

@@ -81,7 +81,7 @@ class ChunkedFileUploader(
 
     init {
         initHttpClient(okHttpClient, currentUser)
-        remoteChunkUrl = ApiUtils.getUrlForChunkedUpload(currentUser.baseUrl, currentUser.userId)
+        remoteChunkUrl = ApiUtils.getUrlForChunkedUpload(currentUser.baseUrl!!, currentUser.userId!!)
     }
 
     @Suppress("Detekt.TooGenericExceptionCaught")
@@ -295,7 +295,7 @@ class ChunkedFileUploader(
                 ApiUtils.getCredentials(
                     currentUser.username,
                     currentUser.token
-                ),
+                )!!,
                 "Authorization"
             )
         )
@@ -304,8 +304,8 @@ class ChunkedFileUploader(
 
     private fun assembleChunks(uploadFolderUri: String, targetPath: String) {
         val destinationUri: String = ApiUtils.getUrlForFileUpload(
-            currentUser.baseUrl,
-            currentUser.userId,
+            currentUser.baseUrl!!,
+            currentUser.userId!!,
             targetPath
         )
         val originUri = "$uploadFolderUri/.file"

+ 1 - 1
app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt

@@ -24,7 +24,7 @@ class FileUploader(
     fun upload(sourceFileUri: Uri, fileName: String, remotePath: String, metaData: String?): Observable<Boolean> {
         return ncApi.uploadFile(
             ApiUtils.getCredentials(currentUser.username, currentUser.token),
-            ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, remotePath),
+            ApiUtils.getUrlForFileUpload(currentUser.baseUrl!!, currentUser.userId!!, remotePath),
             createRequestBody(sourceFileUri)
         )
             .subscribeOn(Schedulers.io())

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt

@@ -69,7 +69,7 @@ object AccountUtils {
     private fun matchAccounts(importAccount: ImportAccount, user: User): Boolean {
         var accountFound = false
         if (importAccount.token != null) {
-            if (UriUtils.hasHttpProtocolPrefixed(importAccount.baseUrl)) {
+            if (UriUtils.hasHttpProtocolPrefixed(importAccount.baseUrl!!)) {
                 if (
                     user.username == importAccount.username &&
                     user.baseUrl == importAccount.baseUrl

+ 0 - 559
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -1,559 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Marcel Hibbe
- * @author Tim Krüger
- * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
- * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.nextcloud.talk.utils;
-
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.nextcloud.talk.BuildConfig;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.data.user.model.User;
-import com.nextcloud.talk.models.RetrofitBucket;
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
-
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import okhttp3.Credentials;
-
-public class ApiUtils {
-    public static final int APIv1 = 1;
-    public static final int APIv2 = 2;
-    public static final int APIv3 = 3;
-    public static final int APIv4 = 4;
-    public static final int AVATAR_SIZE_BIG = 512;
-    public static final int AVATAR_SIZE_SMALL = 64;
-    private static final String TAG = "ApiUtils";
-    private static final String ocsApiVersion = "/ocs/v2.php";
-    private static final String spreedApiVersion = "/apps/spreed/api/v1";
-    private static final String spreedApiBase = ocsApiVersion + "/apps/spreed/api/v";
-
-    private static final String userAgent = "Mozilla/5.0 (Android) Nextcloud-Talk v";
-
-    public static String getUserAgent() {
-        return userAgent + BuildConfig.VERSION_NAME;
-    }
-
-    /**
-     * @deprecated This is only supported on API v1-3, in API v4+ please use
-     * {@link ApiUtils#getUrlForAttendees(int, String, String)} instead.
-     */
-    @Deprecated
-    public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) {
-        String url = getUrlForParticipants(APIv1, baseUrl, roomToken);
-
-        if (isGuest) {
-            url += "/guests";
-        }
-
-        return url;
-    }
-
-    public static RetrofitBucket getRetrofitBucketForContactsSearch(String baseUrl, @Nullable String searchQuery) {
-        RetrofitBucket retrofitBucket = new RetrofitBucket();
-        retrofitBucket.setUrl(baseUrl + ocsApiVersion + "/apps/files_sharing/api/v1/sharees");
-
-        Map<String, String> queryMap = new HashMap<>();
-
-        if (searchQuery == null) {
-            searchQuery = "";
-        }
-        queryMap.put("format", "json");
-        queryMap.put("search", searchQuery);
-        queryMap.put("itemType", "call");
-
-        retrofitBucket.setQueryMap(queryMap);
-
-        return retrofitBucket;
-    }
-
-    public static String getUrlForFilePreviewWithRemotePath(String baseUrl, String remotePath, int px) {
-        return baseUrl + "/index.php/core/preview.png?file="
-            + Uri.encode(remotePath, "UTF-8")
-            + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
-    }
-
-    public static String getUrlForFilePreviewWithFileId(String baseUrl, String fileId, int px) {
-        return baseUrl + "/index.php/core/preview?fileId="
-            + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1";
-    }
-
-    public static String getSharingUrl(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/files_sharing/api/v1/shares";
-    }
-
-    public static RetrofitBucket getRetrofitBucketForContactsSearchFor14(String baseUrl, @Nullable String searchQuery) {
-        RetrofitBucket retrofitBucket = getRetrofitBucketForContactsSearch(baseUrl, searchQuery);
-        retrofitBucket.setUrl(baseUrl + ocsApiVersion + "/core/autocomplete/get");
-
-        retrofitBucket.getQueryMap().put("itemId", "new");
-
-        return retrofitBucket;
-    }
-
-    public static String getUrlForCapabilities(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/cloud/capabilities";
-    }
-
-    public static int getCallApiVersion(User capabilities, int[] versions) throws NoSupportedApiException {
-        return getConversationApiVersion(capabilities, versions);
-    }
-
-    public static int getConversationApiVersion(User user, int[] versions) throws NoSupportedApiException {
-        boolean hasApiV4 = false;
-        for (int version : versions) {
-            hasApiV4 |= version == APIv4;
-        }
-
-        if (!hasApiV4) {
-            Exception e = new Exception("Api call did not try conversation-v4 api");
-            Log.d(TAG, e.getMessage(), e);
-        }
-
-        for (int version : versions) {
-            if (user.hasSpreedFeatureCapability("conversation-v" + version)) {
-                return version;
-            }
-
-            // Fallback for old API versions
-            if ((version == APIv1 || version == APIv2)) {
-                if (user.hasSpreedFeatureCapability("conversation-v2")) {
-                    return version;
-                }
-                if (version == APIv1 &&
-                    user.hasSpreedFeatureCapability("mention-flag") &&
-                    !user.hasSpreedFeatureCapability("conversation-v4")) {
-                    return version;
-                }
-            }
-        }
-        throw new NoSupportedApiException();
-    }
-
-    public static int getSignalingApiVersion(User user, int[] versions) throws NoSupportedApiException {
-        for (int version : versions) {
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "signaling-v" + version)) {
-                return version;
-            }
-
-            if (version == APIv2 &&
-                CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "sip-support") &&
-                !CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "signaling-v3")) {
-                return version;
-            }
-
-            if (version == APIv1 &&
-                !CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "signaling-v3")) {
-                // Has no capability, we just assume it is always there when there is no v3 or later
-                return version;
-            }
-        }
-        throw new NoSupportedApiException();
-    }
-
-    public static int getChatApiVersion(User user, int[] versions) throws NoSupportedApiException {
-        for (int version : versions) {
-            if (version == APIv1 && CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "chat-v2")) {
-                // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil*
-                return version;
-            }
-        }
-        throw new NoSupportedApiException();
-    }
-
-    protected static String getUrlForApi(int version, String baseUrl) {
-        return baseUrl + spreedApiBase + version;
-    }
-
-    public static String getUrlForRooms(int version, String baseUrl) {
-        return getUrlForApi(version, baseUrl) + "/room";
-    }
-
-    public static String getUrlForRoom(int version, String baseUrl, String token) {
-        return getUrlForRooms(version, baseUrl) + "/" + token;
-    }
-
-    public static String getUrlForAttendees(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/attendees";
-    }
-
-    public static String getUrlForParticipants(int version, String baseUrl, String token) {
-        if (token == null || token.isEmpty()) {
-            Log.e(TAG, "token was null or empty");
-        }
-        return getUrlForRoom(version, baseUrl, token) + "/participants";
-    }
-
-    public static String getUrlForParticipantsActive(int version, String baseUrl, String token) {
-        return getUrlForParticipants(version, baseUrl, token) + "/active";
-    }
-
-    public static String getUrlForParticipantsSelf(int version, String baseUrl, String token) {
-        return getUrlForParticipants(version, baseUrl, token) + "/self";
-    }
-
-    public static String getUrlForParticipantsResendInvitations(int version, String baseUrl, String token) {
-        return getUrlForParticipants(version, baseUrl, token) + "/resend-invitations";
-    }
-
-    public static String getUrlForRoomFavorite(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/favorite";
-    }
-
-    public static String getUrlForRoomModerators(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/moderators";
-    }
-
-    public static String getUrlForRoomNotificationLevel(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/notify";
-    }
-
-    public static String getUrlForRoomPublic(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/public";
-    }
-
-    public static String getUrlForRoomPassword(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/password";
-    }
-
-    public static String getUrlForRoomReadOnlyState(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/read-only";
-    }
-
-    public static String getUrlForRoomWebinaryLobby(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/webinar/lobby";
-    }
-
-    public static String getUrlForRoomNotificationCalls(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/notify-calls";
-    }
-
-    public static String getUrlForCall(int version, String baseUrl, String token) {
-        return getUrlForApi(version, baseUrl) + "/call/" + token;
-    }
-
-    public static String getUrlForChat(int version, String baseUrl, String token) {
-        return getUrlForApi(version, baseUrl) + "/chat/" + token;
-    }
-
-    public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) {
-        return getUrlForChat(version, baseUrl, token) + "/mentions";
-    }
-
-    public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) {
-        return getUrlForChat(version, baseUrl, token) + "/" + messageId;
-    }
-
-    public static String getUrlForChatSharedItems(int version, String baseUrl, String token) {
-        return getUrlForChat(version, baseUrl, token) + "/share";
-    }
-
-    public static String getUrlForChatSharedItemsOverview(int version, String baseUrl, String token) {
-        return getUrlForChatSharedItems(version, baseUrl, token) + "/overview";
-    }
-
-    public static String getUrlForSignaling(int version, String baseUrl) {
-        return getUrlForApi(version, baseUrl) + "/signaling";
-    }
-
-    public static String getUrlForSignalingBackend(int version, String baseUrl) {
-        return getUrlForSignaling(version, baseUrl) + "/backend";
-    }
-
-    public static String getUrlForSignalingSettings(int version, String baseUrl) {
-        return getUrlForSignaling(version, baseUrl) + "/settings";
-    }
-
-    public static String getUrlForSignaling(int version, String baseUrl, String token) {
-        return getUrlForSignaling(version, baseUrl) + "/" + token;
-    }
-
-    public static String getUrlForOpenConversations(int version, String baseUrl) {
-        return getUrlForApi(version, baseUrl) + "/listed-room";
-    }
-
-    public static RetrofitBucket getRetrofitBucketForCreateRoom(int version, String baseUrl, String roomType,
-                                                                @Nullable String source,
-                                                                @Nullable String invite,
-                                                                @Nullable String conversationName) {
-        RetrofitBucket retrofitBucket = new RetrofitBucket();
-        retrofitBucket.setUrl(getUrlForRooms(version, baseUrl));
-        Map<String, String> queryMap = new HashMap<>();
-
-        queryMap.put("roomType", roomType);
-        if (invite != null) {
-            queryMap.put("invite", invite);
-        }
-        if (source != null) {
-            queryMap.put("source", source);
-        }
-
-        if (conversationName != null) {
-            queryMap.put("roomName", conversationName);
-        }
-
-        retrofitBucket.setQueryMap(queryMap);
-
-        return retrofitBucket;
-    }
-
-    public static RetrofitBucket getRetrofitBucketForAddParticipant(int version, String baseUrl, String token, String user) {
-        RetrofitBucket retrofitBucket = new RetrofitBucket();
-        retrofitBucket.setUrl(getUrlForParticipants(version, baseUrl, token));
-
-        Map<String, String> queryMap = new HashMap<>();
-
-        queryMap.put("newParticipant", user);
-
-        retrofitBucket.setQueryMap(queryMap);
-
-        return retrofitBucket;
-
-    }
-
-    public static RetrofitBucket getRetrofitBucketForAddParticipantWithSource(
-        int version,
-        String baseUrl,
-        String token,
-        String source,
-        String id
-                                                                             ) {
-        RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, id);
-        retrofitBucket.getQueryMap().put("source", source);
-        return retrofitBucket;
-    }
-
-    public static String getUrlForUserProfile(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/cloud/user";
-    }
-
-    public static String getUrlForUserData(String baseUrl, String userId) {
-        return baseUrl + ocsApiVersion + "/cloud/users/" + userId;
-    }
-
-    public static String getUrlForUserSettings(String baseUrl) {
-        // FIXME Introduce API version
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/settings/user";
-    }
-
-    public static String getUrlPostfixForStatus() {
-        return "/status.php";
-    }
-
-    public static String getUrlForAvatar(String baseUrl, String name, boolean requestBigSize) {
-        int avatarSize = requestBigSize ? AVATAR_SIZE_BIG : AVATAR_SIZE_SMALL;
-        return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize;
-    }
-
-    public static String getUrlForGuestAvatar(String baseUrl, String name, boolean requestBigSize) {
-        int avatarSize = requestBigSize ? AVATAR_SIZE_BIG : AVATAR_SIZE_SMALL;
-        return baseUrl + "/index.php/avatar/guest/" + Uri.encode(name) + "/" + avatarSize;
-    }
-
-    public static String getUrlForConversationAvatar(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/avatar";
-    }
-
-    public static String getUrlForConversationAvatarWithVersion(int version, String baseUrl, String token,
-                                                                boolean isDark,
-                                                                String avatarVersion) {
-        String isDarkString = "";
-        if (isDark) {
-            isDarkString = "/dark";
-        }
-
-        String avatarVersionString = "";
-        if (avatarVersion != null) {
-            avatarVersionString = "?avatarVersion=" + avatarVersion;
-        }
-
-        return getUrlForRoom(version, baseUrl, token) + "/avatar" + isDarkString + avatarVersionString;
-    }
-
-    public static String getCredentials(String username, String token) {
-        if (TextUtils.isEmpty(username) && TextUtils.isEmpty(token)) {
-            return null;
-        }
-        return Credentials.basic(username, token, StandardCharsets.UTF_8);
-    }
-
-    public static String getUrlNextcloudPush(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/push";
-    }
-
-    public static String getUrlPushProxy() {
-        return NextcloudTalkApplication.Companion.getSharedApplication().
-            getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
-    }
-
-    // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
-    public static String getUrlForNcNotificationWithId(String baseUrl, String notificationId) {
-        return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
-    }
-
-    public static String getUrlForSearchByNumber(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
-    }
-
-    public static String getUrlForFileUpload(String baseUrl, String user, String remotePath) {
-        return baseUrl + "/remote.php/dav/files/" + user + remotePath;
-    }
-
-    public static String getUrlForChunkedUpload(String baseUrl, String user) {
-        return baseUrl + "/remote.php/dav/uploads/" + user;
-    }
-
-    public static String getUrlForFileDownload(String baseUrl, String user, String remotePath) {
-        return baseUrl + "/remote.php/dav/files/" + user + "/" + remotePath;
-    }
-
-    public static String getUrlForTempAvatar(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/spreed/temp-user-avatar";
-    }
-
-    public static String getUrlForUserFields(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/cloud/user/fields";
-    }
-
-    public static String getUrlToSendLocation(int version, String baseUrl, String roomToken) {
-        return getUrlForChat(version, baseUrl, roomToken) + "/share";
-    }
-
-    public static String getUrlForHoverCard(String baseUrl, String userId) {
-        return baseUrl + ocsApiVersion +
-            "/hovercard/v1/" + userId;
-    }
-
-    public static String getUrlForChatReadMarker(int version, String baseUrl, String roomToken) {
-        return getUrlForChat(version, baseUrl, roomToken) + "/read";
-    }
-
-    /*
-     * OCS Status API
-     */
-
-    public static String getUrlForStatus(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/user_status";
-    }
-
-    public static String getUrlForSetStatusType(String baseUrl) {
-        return getUrlForStatus(baseUrl) + "/status";
-    }
-
-    public static String getUrlForPredefinedStatuses(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/predefined_statuses";
-    }
-
-    public static String getUrlForStatusMessage(String baseUrl) {
-        return getUrlForStatus(baseUrl) + "/message";
-    }
-
-    public static String getUrlForSetCustomStatus(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/user_status/message/custom";
-    }
-
-    public static String getUrlForSetPredefinedStatus(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/user_status/message/predefined";
-    }
-
-    public static String getUrlForUserStatuses(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/statuses";
-    }
-
-    public static String getUrlForMessageReaction(String baseUrl,
-                                                  String roomToken,
-                                                  String messageId) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/reaction/" + roomToken + "/" + messageId;
-    }
-
-    @NonNull
-    public static String getUrlForUnifiedSearch(@NonNull String baseUrl, @NonNull String providerId) {
-        return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search";
-    }
-
-    public static String getUrlForPoll(String baseUrl,
-                                       String roomToken,
-                                       String pollId) {
-        return getUrlForPoll(baseUrl, roomToken) + "/" + pollId;
-    }
-
-    public static String getUrlForPoll(String baseUrl,
-                                       String roomToken) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/poll/" + roomToken;
-    }
-
-    public static String getUrlForMessageExpiration(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/message-expiration";
-    }
-
-    public static String getUrlForOpenGraph(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/references/resolve";
-    }
-
-    public static String getUrlForRecording(int version, String baseUrl, String token) {
-        return getUrlForApi(version, baseUrl) + "/recording/" + token;
-    }
-
-    public static String getUrlForRequestAssistance(int version, String baseUrl, String token) {
-        return getUrlForApi(version, baseUrl) + "/breakout-rooms/" + token + "/request-assistance";
-    }
-
-    public static String getUrlForConversationDescription(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/description";
-    }
-
-    public static String getUrlForTranslation(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/translation/translate";
-    }
-
-    public static String getUrlForLanguages(String baseUrl) {
-        return baseUrl + ocsApiVersion + "/translation/languages";
-    }
-
-    public static String getUrlForReminder(User user, String roomToken, String messageId, int version) {
-        String url = ApiUtils.getUrlForChatMessage(version, user.getBaseUrl(), roomToken, messageId);
-        return url + "/reminder";
-    }
-
-    public static String getUrlForRecordingConsent(int version, String baseUrl, String token) {
-        return getUrlForRoom(version, baseUrl, token) + "/recording-consent";
-    }
-
-    public static String getUrlForInvitation(String baseUrl) {
-        return baseUrl + ocsApiVersion + spreedApiVersion + "/federation/invitation";
-    }
-
-    public static String getUrlForInvitationAccept(String baseUrl, int id) {
-        return getUrlForInvitation(baseUrl) + "/" + id;
-    }
-
-    public static String getUrlForInvitationReject(String baseUrl, int id) {
-        return getUrlForInvitation(baseUrl) + "/" + id;
-    }
-}

+ 577 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt

@@ -0,0 +1,577 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * @author Tim Krüger
+ * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.utils
+
+import android.net.Uri
+import android.text.TextUtils
+import android.util.Log
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.RetrofitBucket
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
+import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
+import okhttp3.Credentials.basic
+import java.nio.charset.StandardCharsets
+
+@Suppress("TooManyFunctions")
+object ApiUtils {
+    private val TAG = ApiUtils::class.java.simpleName
+    const val API_V1 = 1
+    private const val API_V2 = 2
+    const val API_V3 = 3
+    const val API_V4 = 4
+    private const val AVATAR_SIZE_BIG = 512
+    private const val AVATAR_SIZE_SMALL = 64
+    private const val OCS_API_VERSION = "/ocs/v2.php"
+    private const val SPREED_API_VERSION = "/apps/spreed/api/v1"
+    private const val SPREED_API_BASE = "$OCS_API_VERSION/apps/spreed/api/v"
+
+    @JvmStatic
+    val userAgent = "Mozilla/5.0 (Android) Nextcloud-Talk v"
+        get() = field + BuildConfig.VERSION_NAME
+
+    @Deprecated(
+        "This is only supported on API v1-3, in API v4+ please use " +
+            "{@link ApiUtils#getUrlForAttendees(int, String, String)} instead."
+    )
+    fun getUrlForRemovingParticipantFromConversation(baseUrl: String?, roomToken: String?, isGuest: Boolean): String {
+        var url = getUrlForParticipants(API_V1, baseUrl, roomToken)
+        if (isGuest) {
+            url += "/guests"
+        }
+        return url
+    }
+
+    private fun getRetrofitBucketForContactsSearch(baseUrl: String, searchQuery: String?): RetrofitBucket {
+        var query = searchQuery
+        val retrofitBucket = RetrofitBucket()
+        retrofitBucket.url = "$baseUrl$OCS_API_VERSION/apps/files_sharing/api/v1/sharees"
+        val queryMap: MutableMap<String, String> = HashMap()
+        if (query == null) {
+            query = ""
+        }
+        queryMap["format"] = "json"
+        queryMap["search"] = query
+        queryMap["itemType"] = "call"
+        retrofitBucket.queryMap = queryMap
+        return retrofitBucket
+    }
+
+    fun getUrlForFilePreviewWithRemotePath(baseUrl: String, remotePath: String?, px: Int): String {
+        return (
+            baseUrl + "/index.php/core/preview.png?file=" +
+                Uri.encode(remotePath, "UTF-8") +
+                "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"
+            )
+    }
+
+    fun getUrlForFilePreviewWithFileId(baseUrl: String, fileId: String, px: Int): String {
+        return (
+            baseUrl + "/index.php/core/preview?fileId=" +
+                fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"
+            )
+    }
+
+    fun getSharingUrl(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/files_sharing/api/v1/shares"
+    }
+
+    fun getRetrofitBucketForContactsSearchFor14(baseUrl: String, searchQuery: String?): RetrofitBucket {
+        val retrofitBucket = getRetrofitBucketForContactsSearch(baseUrl, searchQuery)
+        retrofitBucket.url = "$baseUrl$OCS_API_VERSION/core/autocomplete/get"
+        retrofitBucket.queryMap?.put("itemId", "new")
+        return retrofitBucket
+    }
+
+    @JvmStatic
+    fun getUrlForCapabilities(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/cloud/capabilities"
+    }
+
+    @Throws(NoSupportedApiException::class)
+    fun getCallApiVersion(capabilities: User, versions: IntArray): Int {
+        return getConversationApiVersion(capabilities, versions)
+    }
+
+    @JvmStatic
+    @Throws(NoSupportedApiException::class)
+    @Suppress("ReturnCount")
+    fun getConversationApiVersion(user: User, versions: IntArray): Int {
+        var hasApiV4 = false
+        for (version in versions) {
+            hasApiV4 = hasApiV4 or (version == API_V4)
+        }
+        if (!hasApiV4) {
+            val e = Exception("Api call did not try conversation-v4 api")
+            Log.d(TAG, e.message, e)
+        }
+        for (version in versions) {
+            if (user.hasSpreedFeatureCapability("conversation-v$version")) {
+                return version
+            }
+
+            // Fallback for old API versions
+            if (version == API_V1 || version == API_V2) {
+                if (user.hasSpreedFeatureCapability("conversation-v2")) {
+                    return version
+                }
+                if (version == API_V1 &&
+                    user.hasSpreedFeatureCapability("mention-flag") &&
+                    !user.hasSpreedFeatureCapability("conversation-v4")
+                ) {
+                    return version
+                }
+            }
+        }
+        throw NoSupportedApiException()
+    }
+
+    @JvmStatic
+    @Throws(NoSupportedApiException::class)
+    @Suppress("ReturnCount")
+    fun getSignalingApiVersion(user: User, versions: IntArray): Int {
+        val spreedCapabilities = user.capabilities!!.spreedCapability
+        for (version in versions) {
+            if (spreedCapabilities != null) {
+                if (hasSpreedFeatureCapability(spreedCapabilities, "signaling-v$version")) {
+                    return version
+                }
+                if (version == API_V2 &&
+                    hasSpreedFeatureCapability(spreedCapabilities, "sip-support") &&
+                    !hasSpreedFeatureCapability(spreedCapabilities, "signaling-v3")
+                ) {
+                    return version
+                }
+                if (version == API_V1 &&
+                    !hasSpreedFeatureCapability(spreedCapabilities, "signaling-v3")
+                ) {
+                    // Has no capability, we just assume it is always there when there is no v3 or later
+                    return version
+                }
+            }
+        }
+        throw NoSupportedApiException()
+    }
+
+    @JvmStatic
+    @Throws(NoSupportedApiException::class)
+    fun getChatApiVersion(spreedCapabilities: SpreedCapability, versions: IntArray): Int {
+        for (version in versions) {
+            if (version == API_V1 && hasSpreedFeatureCapability(spreedCapabilities, "chat-v2")) {
+                // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil*
+                return version
+            }
+        }
+        throw NoSupportedApiException()
+    }
+
+    private fun getUrlForApi(version: Int, baseUrl: String?): String {
+        return baseUrl + SPREED_API_BASE + version
+    }
+
+    fun getUrlForRooms(version: Int, baseUrl: String?): String {
+        return getUrlForApi(version, baseUrl) + "/room"
+    }
+
+    @JvmStatic
+    fun getUrlForRoom(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRooms(version, baseUrl) + "/" + token
+    }
+
+    fun getUrlForAttendees(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/attendees"
+    }
+
+    fun getUrlForParticipants(version: Int, baseUrl: String?, token: String?): String {
+        if (token.isNullOrEmpty()) {
+            Log.e(TAG, "token was null or empty")
+        }
+        return getUrlForRoom(version, baseUrl, token) + "/participants"
+    }
+
+    fun getUrlForParticipantsActive(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForParticipants(version, baseUrl, token) + "/active"
+    }
+
+    @JvmStatic
+    fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForParticipants(version, baseUrl, token) + "/self"
+    }
+
+    fun getUrlForParticipantsResendInvitations(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForParticipants(version, baseUrl, token) + "/resend-invitations"
+    }
+
+    fun getUrlForRoomFavorite(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/favorite"
+    }
+
+    fun getUrlForRoomModerators(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/moderators"
+    }
+
+    @JvmStatic
+    fun getUrlForRoomNotificationLevel(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/notify"
+    }
+
+    fun getUrlForRoomPublic(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/public"
+    }
+
+    fun getUrlForRoomPassword(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/password"
+    }
+
+    fun getUrlForRoomReadOnlyState(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/read-only"
+    }
+
+    fun getUrlForRoomWebinaryLobby(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/webinar/lobby"
+    }
+
+    @JvmStatic
+    fun getUrlForRoomNotificationCalls(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/notify-calls"
+    }
+
+    fun getUrlForCall(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForApi(version, baseUrl) + "/call/" + token
+    }
+
+    fun getUrlForChat(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForApi(version, baseUrl) + "/chat/" + token
+    }
+
+    @JvmStatic
+    fun getUrlForMentionSuggestions(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForChat(version, baseUrl, token) + "/mentions"
+    }
+
+    fun getUrlForChatMessage(version: Int, baseUrl: String?, token: String, messageId: String): String {
+        return getUrlForChat(version, baseUrl, token) + "/" + messageId
+    }
+
+    fun getUrlForChatSharedItems(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForChat(version, baseUrl, token) + "/share"
+    }
+
+    fun getUrlForChatSharedItemsOverview(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForChatSharedItems(version, baseUrl, token) + "/overview"
+    }
+
+    fun getUrlForSignaling(version: Int, baseUrl: String?): String {
+        return getUrlForApi(version, baseUrl) + "/signaling"
+    }
+
+    @JvmStatic
+    fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String {
+        return getUrlForSignaling(version, baseUrl) + "/backend"
+    }
+
+    @JvmStatic
+    fun getUrlForSignalingSettings(version: Int, baseUrl: String?): String {
+        return getUrlForSignaling(version, baseUrl) + "/settings"
+    }
+
+    fun getUrlForSignaling(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForSignaling(version, baseUrl) + "/" + token
+    }
+
+    fun getUrlForOpenConversations(version: Int, baseUrl: String?): String {
+        return getUrlForApi(version, baseUrl) + "/listed-room"
+    }
+
+    @Suppress("LongParameterList")
+    fun getRetrofitBucketForCreateRoom(
+        version: Int,
+        baseUrl: String?,
+        roomType: String,
+        source: String?,
+        invite: String?,
+        conversationName: String?
+    ): RetrofitBucket {
+        val retrofitBucket = RetrofitBucket()
+        retrofitBucket.url = getUrlForRooms(version, baseUrl)
+        val queryMap: MutableMap<String, String> = HashMap()
+        queryMap["roomType"] = roomType
+        if (invite != null) {
+            queryMap["invite"] = invite
+        }
+        if (source != null) {
+            queryMap["source"] = source
+        }
+        if (conversationName != null) {
+            queryMap["roomName"] = conversationName
+        }
+        retrofitBucket.queryMap = queryMap
+        return retrofitBucket
+    }
+
+    @JvmStatic
+    fun getRetrofitBucketForAddParticipant(
+        version: Int,
+        baseUrl: String?,
+        token: String?,
+        user: String
+    ): RetrofitBucket {
+        val retrofitBucket = RetrofitBucket()
+        retrofitBucket.url = getUrlForParticipants(version, baseUrl, token)
+        val queryMap: MutableMap<String, String> = HashMap()
+        queryMap["newParticipant"] = user
+        retrofitBucket.queryMap = queryMap
+        return retrofitBucket
+    }
+
+    @JvmStatic
+    fun getRetrofitBucketForAddParticipantWithSource(
+        version: Int,
+        baseUrl: String?,
+        token: String?,
+        source: String,
+        id: String
+    ): RetrofitBucket {
+        val retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, id)
+        retrofitBucket.queryMap?.put("source", source)
+        return retrofitBucket
+    }
+
+    fun getUrlForUserProfile(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/cloud/user"
+    }
+
+    fun getUrlForUserData(baseUrl: String, userId: String): String {
+        return "$baseUrl$OCS_API_VERSION/cloud/users/$userId"
+    }
+
+    fun getUrlForUserSettings(baseUrl: String): String {
+        // FIXME Introduce API version
+        return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/settings/user"
+    }
+
+    fun getUrlPostfixForStatus(): String {
+        return "/status.php"
+    }
+
+    @JvmStatic
+    fun getUrlForAvatar(baseUrl: String?, name: String?, requestBigSize: Boolean): String {
+        val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL
+        return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize
+    }
+
+    @JvmStatic
+    fun getUrlForGuestAvatar(baseUrl: String?, name: String?, requestBigSize: Boolean): String {
+        val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL
+        return baseUrl + "/index.php/avatar/guest/" + Uri.encode(name) + "/" + avatarSize
+    }
+
+    fun getUrlForConversationAvatar(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/avatar"
+    }
+
+    fun getUrlForConversationAvatarWithVersion(
+        version: Int,
+        baseUrl: String?,
+        token: String?,
+        isDark: Boolean,
+        avatarVersion: String?
+    ): String {
+        var isDarkString = ""
+        if (isDark) {
+            isDarkString = "/dark"
+        }
+        var avatarVersionString = ""
+        if (avatarVersion != null) {
+            avatarVersionString = "?avatarVersion=$avatarVersion"
+        }
+        return getUrlForRoom(version, baseUrl, token) + "/avatar" + isDarkString + avatarVersionString
+    }
+
+    @JvmStatic
+    fun getCredentials(username: String?, token: String?): String? {
+        return if (TextUtils.isEmpty(username) && TextUtils.isEmpty(token)) {
+            null
+        } else {
+            basic(username!!, token!!, StandardCharsets.UTF_8)
+        }
+    }
+
+    @JvmStatic
+    fun getUrlNextcloudPush(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/notifications/api/v2/push"
+    }
+
+    @JvmStatic
+    fun getUrlPushProxy(): String {
+        return sharedApplication!!.applicationContext.resources.getString(R.string.nc_push_server_url) + "/devices"
+    }
+
+    // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
+    fun getUrlForNcNotificationWithId(baseUrl: String, notificationId: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/notifications/api/v2/notifications/$notificationId"
+    }
+
+    fun getUrlForSearchByNumber(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone"
+    }
+
+    fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String {
+        return "$baseUrl/remote.php/dav/files/$user$remotePath"
+    }
+
+    fun getUrlForChunkedUpload(baseUrl: String, user: String): String {
+        return "$baseUrl/remote.php/dav/uploads/$user"
+    }
+
+    fun getUrlForFileDownload(baseUrl: String, user: String, remotePath: String): String {
+        return "$baseUrl/remote.php/dav/files/$user/$remotePath"
+    }
+
+    fun getUrlForTempAvatar(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/spreed/temp-user-avatar"
+    }
+
+    fun getUrlForUserFields(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/cloud/user/fields"
+    }
+
+    fun getUrlToSendLocation(version: Int, baseUrl: String?, roomToken: String): String {
+        return getUrlForChat(version, baseUrl, roomToken) + "/share"
+    }
+
+    fun getUrlForHoverCard(baseUrl: String, userId: String): String {
+        return baseUrl + OCS_API_VERSION +
+            "/hovercard/v1/" + userId
+    }
+
+    fun getUrlForChatReadMarker(version: Int, baseUrl: String?, roomToken: String): String {
+        return getUrlForChat(version, baseUrl, roomToken) + "/read"
+    }
+
+    /*
+     * OCS Status API
+     */
+    @JvmStatic
+    fun getUrlForStatus(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status"
+    }
+
+    fun getUrlForSetStatusType(baseUrl: String): String {
+        return getUrlForStatus(baseUrl) + "/status"
+    }
+
+    fun getUrlForPredefinedStatuses(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/predefined_statuses"
+    }
+
+    fun getUrlForStatusMessage(baseUrl: String): String {
+        return getUrlForStatus(baseUrl) + "/message"
+    }
+
+    fun getUrlForSetCustomStatus(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/message/custom"
+    }
+
+    fun getUrlForSetPredefinedStatus(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/message/predefined"
+    }
+
+    fun getUrlForUserStatuses(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/statuses"
+    }
+
+    fun getUrlForMessageReaction(baseUrl: String, roomToken: String, messageId: String): String {
+        return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/reaction/$roomToken/$messageId"
+    }
+
+    fun getUrlForUnifiedSearch(baseUrl: String, providerId: String): String {
+        return "$baseUrl$OCS_API_VERSION/search/providers/$providerId/search"
+    }
+
+    fun getUrlForPoll(baseUrl: String, roomToken: String, pollId: String): String {
+        return getUrlForPoll(baseUrl, roomToken) + "/" + pollId
+    }
+
+    fun getUrlForPoll(baseUrl: String, roomToken: String): String {
+        return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/poll/$roomToken"
+    }
+
+    @JvmStatic
+    fun getUrlForMessageExpiration(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/message-expiration"
+    }
+
+    fun getUrlForOpenGraph(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/references/resolve"
+    }
+
+    fun getUrlForRecording(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForApi(version, baseUrl) + "/recording/" + token
+    }
+
+    fun getUrlForRequestAssistance(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForApi(version, baseUrl) + "/breakout-rooms/" + token + "/request-assistance"
+    }
+
+    fun getUrlForConversationDescription(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/description"
+    }
+
+    fun getUrlForTranslation(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/translation/translate"
+    }
+
+    fun getUrlForLanguages(baseUrl: String): String {
+        return "$baseUrl$OCS_API_VERSION/translation/languages"
+    }
+
+    fun getUrlForReminder(user: User, roomToken: String, messageId: String, version: Int): String {
+        val url = getUrlForChatMessage(version, user.baseUrl!!, roomToken, messageId)
+        return "$url/reminder"
+    }
+
+    fun getUrlForRecordingConsent(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRoom(version, baseUrl, token) + "/recording-consent"
+    }
+
+    fun getUrlForInvitation(baseUrl: String): String {
+        return baseUrl + OCS_API_VERSION + SPREED_API_VERSION + "/federation/invitation"
+    }
+
+    fun getUrlForInvitationAccept(baseUrl: String, id: Int): String {
+        return getUrlForInvitation(baseUrl) + "/" + id
+    }
+
+    fun getUrlForInvitationReject(baseUrl: String, id: Int): String {
+        return getUrlForInvitation(baseUrl) + "/" + id
+    }
+
+    @JvmStatic
+    fun getUrlForRoomCapabilities(version: Int, baseUrl: String?, token: String?): String {
+        return getUrlForRooms(version, baseUrl) + "/" + token + "/capabilities"
+    }
+}

+ 275 - 0
app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt

@@ -0,0 +1,275 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2023-2024 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.utils
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
+
+enum class SpreedFeatures(val value: String) {
+    RECORDING_V1("recording-v1"),
+    REACTIONS("reactions"),
+    RAISE_HAND("raise-hand"),
+    DIRECT_MENTION_FLAG("direct-mention-flag"),
+    CONVERSATION_CALL_FLAGS("conversation-call-flags"),
+    SILENT_SEND("silent-send"),
+    MENTION_FLAG("mention-flag"),
+    DELETE_MESSAGES("delete-messages"),
+    READ_ONLY_ROOMS("read-only-rooms"),
+    RICH_OBJECT_LIST_MEDIA("rich-object-list-media"),
+    SILENT_CALL("silent-call"),
+    MESSAGE_EXPIRATION("message-expiration"),
+    WEBINARY_LOBBY("webinary-lobby"),
+    VOICE_MESSAGE_SHARING("voice-message-sharing"),
+    INVITE_GROUPS_AND_MAILS("invite-groups-and-mails"),
+    CIRCLES_SUPPORT("circles-support"),
+    LAST_ROOM_ACTIVITY("last-room-activity"),
+    NOTIFICATION_LEVELS("notification-levels"),
+    CLEAR_HISTORY("clear-history"),
+    AVATAR("avatar"),
+    LISTABLE_ROOMS("listable-rooms"),
+    LOCKED_ONE_TO_ONE_ROOMS("locked-one-to-one-rooms"),
+    TEMP_USER_AVATAR_API("temp-user-avatar-api"),
+    PHONEBOOK_SEARCH("phonebook-search"),
+    GEO_LOCATION_SHARING("geo-location-sharing"),
+    TALK_POLLS("talk-polls")
+}
+
+@Suppress("TooManyFunctions")
+object CapabilitiesUtil {
+
+    //region Version checks
+    fun isServerEOL(serverVersion: Int): Boolean {
+        return (serverVersion < SERVER_VERSION_MIN_SUPPORTED)
+    }
+
+    fun isServerAlmostEOL(serverVersion: Int): Boolean {
+        return (serverVersion < SERVER_VERSION_SUPPORT_WARNING)
+    }
+
+    // endregion
+
+    //region CoreCapabilities
+
+    @JvmStatic
+    fun isLinkPreviewAvailable(user: User): Boolean {
+        return user.capabilities?.coreCapability?.referenceApi != null &&
+            user.capabilities?.coreCapability?.referenceApi == "true"
+    }
+
+    // endregion
+
+    //region SpreedCapabilities
+
+    @JvmStatic
+    fun hasSpreedFeatureCapability(spreedCapabilities: SpreedCapability, spreedFeatures: SpreedFeatures): Boolean {
+        if (spreedCapabilities.features != null) {
+            return spreedCapabilities.features!!.contains(spreedFeatures.value)
+        }
+        return false
+    }
+
+    @JvmStatic
+    @Deprecated("Add your capability to Capability enums and use hasSpreedFeatureCapability with enum.")
+    fun hasSpreedFeatureCapability(spreedCapabilities: SpreedCapability, capabilityName: String): Boolean {
+        if (spreedCapabilities.features != null) {
+            return spreedCapabilities.features!!.contains(capabilityName)
+        }
+        return false
+    }
+
+    fun getMessageMaxLength(spreedCapabilities: SpreedCapability): Int {
+        if (spreedCapabilities.config?.containsKey("chat") == true) {
+            val chatConfigHashMap = spreedCapabilities.config!!["chat"]
+            if (chatConfigHashMap?.containsKey("max-length") == true) {
+                val chatSize = (chatConfigHashMap["max-length"]!!.toString()).toInt()
+                return if (chatSize > 0) {
+                    chatSize
+                } else {
+                    DEFAULT_CHAT_SIZE
+                }
+            }
+        }
+
+        return DEFAULT_CHAT_SIZE
+    }
+
+    fun isReadStatusAvailable(spreedCapabilities: SpreedCapability): Boolean {
+        if (spreedCapabilities.config?.containsKey("chat") == true) {
+            val map: Map<String, Any>? = spreedCapabilities.config!!["chat"]
+            return map != null && map.containsKey("read-privacy")
+        }
+        return false
+    }
+
+    @JvmStatic
+    fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean {
+        if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) &&
+            spreedCapabilities.config?.containsKey("call") == true
+        ) {
+            val map: Map<String, Any>? = spreedCapabilities.config!!["call"]
+            if (map != null && map.containsKey("recording")) {
+                return (map["recording"].toString()).toBoolean()
+            }
+        }
+        return false
+    }
+
+    @JvmStatic
+    fun getAttachmentFolder(spreedCapabilities: SpreedCapability): String {
+        if (spreedCapabilities.config?.containsKey("attachments") == true) {
+            val map = spreedCapabilities.config!!["attachments"]
+            if (map?.containsKey("folder") == true) {
+                return map["folder"].toString()
+            }
+        }
+        return "/Talk"
+    }
+
+    fun isConversationDescriptionEndpointAvailable(spreedCapabilities: SpreedCapability): Boolean {
+        return hasSpreedFeatureCapability(spreedCapabilities, "room-description")
+    }
+
+    fun isUnifiedSearchAvailable(spreedCapabilities: SpreedCapability): Boolean {
+        return hasSpreedFeatureCapability(spreedCapabilities, "unified-search")
+    }
+
+    fun isAbleToCall(spreedCapabilities: SpreedCapability): Boolean {
+        return if (
+            spreedCapabilities.config?.containsKey("call") == true &&
+            spreedCapabilities.config!!["call"] != null &&
+            spreedCapabilities.config!!["call"]!!.containsKey("enabled")
+        ) {
+            java.lang.Boolean.parseBoolean(spreedCapabilities.config!!["call"]!!["enabled"].toString())
+        } else {
+            // older nextcloud versions without the capability can't disable the calls
+            true
+        }
+    }
+
+    fun isCallReactionsSupported(user: User?): Boolean {
+        if (user?.capabilities != null) {
+            val capabilities = user.capabilities
+            return capabilities?.spreedCapability?.config?.containsKey("call") == true &&
+                capabilities.spreedCapability!!.config!!["call"] != null &&
+                capabilities.spreedCapability!!.config!!["call"]!!.containsKey("supported-reactions")
+        }
+        return false
+    }
+
+    fun isTranslationsSupported(spreedCapabilities: SpreedCapability): Boolean {
+        return spreedCapabilities.config?.containsKey("chat") == true &&
+            spreedCapabilities.config!!["chat"] != null &&
+            spreedCapabilities.config!!["chat"]!!.containsKey("has-translation-providers") &&
+            spreedCapabilities.config!!["chat"]!!["has-translation-providers"] == true
+    }
+
+    fun getRecordingConsentType(spreedCapabilities: SpreedCapability): Int {
+        if (
+            spreedCapabilities.config?.containsKey("call") == true &&
+            spreedCapabilities.config!!["call"] != null &&
+            spreedCapabilities.config!!["call"]!!.containsKey("recording-consent")
+        ) {
+            return when (
+                spreedCapabilities.config!!["call"]!!["recording-consent"].toString()
+                    .toInt()
+            ) {
+                1 -> RECORDING_CONSENT_REQUIRED
+                2 -> RECORDING_CONSENT_DEPEND_ON_CONVERSATION
+                else -> RECORDING_CONSENT_NOT_REQUIRED
+            }
+        }
+        return RECORDING_CONSENT_NOT_REQUIRED
+    }
+
+    // endregion
+
+    //region SpreedCapabilities that can't be used with federation as the settings for them are global
+
+    fun isReadStatusPrivate(user: User): Boolean {
+        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
+            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
+            if (map?.containsKey("read-privacy") == true) {
+                return (map["read-privacy"]!!.toString()).toInt() == 1
+            }
+        }
+        return false
+    }
+
+    fun isTypingStatusAvailable(user: User): Boolean {
+        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
+            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
+            return map != null && map.containsKey("typing-privacy")
+        }
+        return false
+    }
+
+    fun isTypingStatusPrivate(user: User): Boolean {
+        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
+            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
+            if (map?.containsKey("typing-privacy") == true) {
+                return (map["typing-privacy"]!!.toString()).toInt() == 1
+            }
+        }
+        return false
+    }
+
+    // endregion
+
+    //region ThemingCapabilities
+
+    fun getServerName(user: User?): String? {
+        if (user?.capabilities?.themingCapability != null) {
+            return user.capabilities!!.themingCapability!!.name
+        }
+        return ""
+    }
+
+    // endregion
+
+    //region ProvisioningCapabilities
+
+    fun canEditScopes(user: User): Boolean {
+        return user.capabilities?.provisioningCapability?.accountPropertyScopesVersion != null &&
+            user.capabilities!!.provisioningCapability!!.accountPropertyScopesVersion!! > 1
+    }
+
+    // endregion
+
+    //region UserStatusCapabilities
+
+    @JvmStatic
+    fun isUserStatusAvailable(user: User): Boolean {
+        return user.capabilities?.userStatusCapability?.enabled == true &&
+            user.capabilities?.userStatusCapability?.supportsEmoji == true
+    }
+
+    // endregion
+
+    const val DEFAULT_CHAT_SIZE = 1000
+    const val RECORDING_CONSENT_NOT_REQUIRED = 0
+    const val RECORDING_CONSENT_REQUIRED = 1
+    const val RECORDING_CONSENT_DEPEND_ON_CONVERSATION = 2
+    private const val SERVER_VERSION_MIN_SUPPORTED = 14
+    private const val SERVER_VERSION_SUPPORT_WARNING = 18
+}

+ 11 - 12
app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt

@@ -1,10 +1,9 @@
 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
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 
 /*
  * Nextcloud Talk application
@@ -45,28 +44,28 @@ object ConversationUtils {
             ParticipantType.MODERATOR == conversation.participantType
     }
 
-    private fun isLockedOneToOne(conversation: ConversationModel, conversationUser: User): Boolean {
+    fun isLockedOneToOne(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean {
         return conversation.type == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
-            CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "locked-one-to-one-rooms")
+            CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, "locked-one-to-one-rooms")
     }
 
-    fun canModerate(conversation: ConversationModel, conversationUser: User): Boolean {
+    fun canModerate(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean {
         return isParticipantOwnerOrModerator(conversation) &&
-            !isLockedOneToOne(conversation, conversationUser) &&
+            !isLockedOneToOne(conversation, spreedCapabilities) &&
             conversation.type != ConversationType.FORMER_ONE_TO_ONE &&
             !isNoteToSelfConversation(conversation)
     }
 
-    fun isLobbyViewApplicable(conversation: ConversationModel, conversationUser: User): Boolean {
-        return !canModerate(conversation, conversationUser) &&
+    fun isLobbyViewApplicable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean {
+        return !canModerate(conversation, spreedCapabilities) &&
             (
                 conversation.type == ConversationType.ROOM_GROUP_CALL ||
                     conversation.type == ConversationType.ROOM_PUBLIC_CALL
                 )
     }
 
-    fun isNameEditable(conversation: ConversationModel, conversationUser: User): Boolean {
-        return canModerate(conversation, conversationUser) &&
+    fun isNameEditable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean {
+        return canModerate(conversation, spreedCapabilities) &&
             ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation.type
     }
 
@@ -79,12 +78,12 @@ object ConversationUtils {
         }
     }
 
-    fun canDelete(conversation: ConversationModel, conversationUser: User): Boolean {
+    fun canDelete(conversation: ConversationModel, spreedCapability: SpreedCapability): Boolean {
         return if (conversation.canDeleteConversation != null) {
             // Available since APIv2
             conversation.canDeleteConversation!!
         } else {
-            canModerate(conversation, conversationUser)
+            canModerate(conversation, spreedCapability)
             // Fallback for APIv1
         }
     }

+ 1 - 2
app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt

@@ -61,7 +61,6 @@ import com.nextcloud.talk.utils.MimetypeUtils.isGif
 import com.nextcloud.talk.utils.MimetypeUtils.isMarkdown
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
 import java.io.File
 import java.util.concurrent.ExecutionException
 
@@ -308,7 +307,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
             .putString(DownloadFileToCacheWorker.KEY_USER_ID, user.userId)
             .putString(
                 DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER,
-                CapabilitiesUtilNew.getAttachmentFolder(user)
+                CapabilitiesUtil.getAttachmentFolder(user.capabilities!!.spreedCapability!!)
             )
             .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileInfo.fileName)
             .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)

+ 7 - 8
app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt

@@ -22,22 +22,21 @@
 
 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.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.conversations.Conversation
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
 
 /**
  * see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions
  */
 class ParticipantPermissions(
-    private val user: User,
+    private val spreedCapabilities: SpreedCapability,
     private val conversation: ConversationModel
 ) {
 
     @Deprecated("Use ChatRepository.ConversationModel")
-    constructor(user: User, conversation: Conversation) : this(
-        user,
+    constructor(spreedCapabilities: SpreedCapability, conversation: Conversation) : this(
+        spreedCapabilities,
         ConversationModel.mapToConversationModel(conversation)
     )
 
@@ -52,8 +51,8 @@ class ParticipantPermissions(
     private val hasChatPermission = (conversation.permissions and CHAT) == CHAT
 
     private fun hasConversationPermissions(): Boolean {
-        return CapabilitiesUtilNew.hasSpreedFeatureCapability(
-            user,
+        return CapabilitiesUtil.hasSpreedFeatureCapability(
+            spreedCapabilities,
             "conversation-permissions"
         )
     }
@@ -91,7 +90,7 @@ class ParticipantPermissions(
     }
 
     fun hasChatPermission(): Boolean {
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "chat-permission")) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, "chat-permission")) {
             return hasChatPermission
         }
         // if capability is not available then the spreed version doesn't support to restrict this

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt

@@ -238,7 +238,7 @@ class PushUtils {
         val credentials = ApiUtils.getCredentials(user.username, user.token)
         ncApi.registerDeviceForNotificationsWithNextcloud(
             credentials,
-            ApiUtils.getUrlNextcloudPush(user.baseUrl),
+            ApiUtils.getUrlNextcloudPush(user.baseUrl!!),
             nextcloudRegisterPushMap
         )
             .subscribe(object : Observer<PushRegistrationOverall> {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt

@@ -53,8 +53,8 @@ object RemoteFileUtils {
         return ncApi.checkIfFileExists(
             ApiUtils.getCredentials(currentUser.username, currentUser.token),
             ApiUtils.getUrlForFileUpload(
-                currentUser.baseUrl,
-                currentUser.userId,
+                currentUser.baseUrl!!,
+                currentUser.userId!!,
                 remotePath
             )
         )

+ 2 - 2
app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt

@@ -22,10 +22,10 @@ package com.nextcloud.talk.utils
 import android.content.Context
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.domain.ConversationModel
 
 object ShareUtils {
-    fun getStringForIntent(context: Context, user: User, conversation: Conversation?): String {
+    fun getStringForIntent(context: Context, user: User, conversation: ConversationModel?): String {
         return String.format(
             context.resources.getString(R.string.nc_share_text),
             user.baseUrl,

+ 1 - 0
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt

@@ -89,4 +89,5 @@ object BundleKeys {
     const val KEY_REAUTHORIZE_ACCOUNT = "KEY_REAUTHORIZE_ACCOUNT"
     const val KEY_PASSWORD = "KEY_PASSWORD"
     const val KEY_REMOTE_TALK_SHARE = "KEY_REMOTE_TALK_SHARE"
+    const val KEY_CHAT_API_VERSION = "KEY_CHAT_API_VERSION"
 }

+ 0 - 267
app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt

@@ -1,267 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Andy Scherzinger
- * @author Mario Danic
- * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.nextcloud.talk.utils.database.user
-
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.models.json.capabilities.Capabilities
-
-@Suppress("TooManyFunctions")
-object CapabilitiesUtilNew {
-    fun hasNotificationsCapability(user: User, capabilityName: String): Boolean {
-        return user.capabilities?.spreedCapability?.features?.contains(capabilityName) == true
-    }
-
-    fun hasExternalCapability(user: User, capabilityName: String?): Boolean {
-        if (user.capabilities?.externalCapability?.containsKey("v1") == true) {
-            return user.capabilities!!.externalCapability!!["v1"]?.contains(capabilityName!!) == true
-        }
-        return false
-    }
-
-    @JvmStatic
-    fun isServerEOL(capabilities: Capabilities?): Boolean {
-        // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018
-        return !hasSpreedFeatureCapability(capabilities, "no-ping")
-    }
-
-    fun isServerAlmostEOL(user: User): Boolean {
-        // Capability is available since Talk 8 => Nextcloud 18 => January 2020
-        return !hasSpreedFeatureCapability(user, "chat-replies")
-    }
-
-    fun canSetChatReadMarker(user: User): Boolean {
-        return hasSpreedFeatureCapability(user, "chat-read-marker")
-    }
-
-    fun canMarkRoomAsUnread(user: User): Boolean {
-        return hasSpreedFeatureCapability(user, "chat-unread")
-    }
-
-    @JvmStatic
-    fun hasSpreedFeatureCapability(user: User?, capabilityName: String): Boolean {
-        return hasSpreedFeatureCapability(user?.capabilities, capabilityName)
-    }
-
-    @JvmStatic
-    fun hasSpreedFeatureCapability(capabilities: Capabilities?, capabilityName: String): Boolean {
-        if (capabilities?.spreedCapability?.features != null) {
-            return capabilities.spreedCapability!!.features!!.contains(capabilityName)
-        }
-        return false
-    }
-
-    fun getMessageMaxLength(user: User?): Int {
-        if (user?.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
-            val chatConfigHashMap = user.capabilities!!.spreedCapability!!.config!!["chat"]
-            if (chatConfigHashMap?.containsKey("max-length") == true) {
-                val chatSize = (chatConfigHashMap["max-length"]!!.toString()).toInt()
-                return if (chatSize > 0) {
-                    chatSize
-                } else {
-                    DEFAULT_CHAT_SIZE
-                }
-            }
-        }
-
-        return DEFAULT_CHAT_SIZE
-    }
-
-    fun isPhoneBookIntegrationAvailable(user: User): Boolean {
-        return user.capabilities?.spreedCapability?.features?.contains("phonebook-search") == true
-    }
-
-    fun isReadStatusAvailable(user: User): Boolean {
-        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
-            val map: Map<String, Any>? = user.capabilities!!.spreedCapability!!.config!!["chat"]
-            return map != null && map.containsKey("read-privacy")
-        }
-        return false
-    }
-
-    fun isReadStatusPrivate(user: User): Boolean {
-        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
-            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
-            if (map?.containsKey("read-privacy") == true) {
-                return (map["read-privacy"]!!.toString()).toInt() == 1
-            }
-        }
-        return false
-    }
-
-    fun isTypingStatusAvailable(user: User): Boolean {
-        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
-            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
-            return map != null && map.containsKey("typing-privacy")
-        }
-        return false
-    }
-
-    fun isTypingStatusPrivate(user: User): Boolean {
-        if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) {
-            val map = user.capabilities!!.spreedCapability!!.config!!["chat"]
-            if (map?.containsKey("typing-privacy") == true) {
-                return (map["typing-privacy"]!!.toString()).toInt() == 1
-            }
-        }
-        return false
-    }
-
-    @JvmStatic
-    fun isCallRecordingAvailable(user: User): Boolean {
-        if (hasSpreedFeatureCapability(user, "recording-v1") &&
-            user.capabilities?.spreedCapability?.config?.containsKey("call") == true
-        ) {
-            val map: Map<String, Any>? = user.capabilities!!.spreedCapability!!.config!!["call"]
-            if (map != null && map.containsKey("recording")) {
-                return (map["recording"].toString()).toBoolean()
-            }
-        }
-        return false
-    }
-
-    @JvmStatic
-    fun isUserStatusAvailable(user: User): Boolean {
-        return user.capabilities?.userStatusCapability?.enabled == true &&
-            user.capabilities?.userStatusCapability?.supportsEmoji == true
-    }
-
-    @JvmStatic
-    fun getAttachmentFolder(user: User): String {
-        if (user.capabilities?.spreedCapability?.config?.containsKey("attachments") == true) {
-            val map = user.capabilities!!.spreedCapability!!.config!!["attachments"]
-            if (map?.containsKey("folder") == true) {
-                return map["folder"].toString()
-            }
-        }
-        return "/Talk"
-    }
-
-    fun getServerName(user: User?): String? {
-        if (user?.capabilities?.themingCapability != null) {
-            return user.capabilities!!.themingCapability!!.name
-        }
-        return ""
-    }
-
-    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
-    fun isAvatarEndpointAvailable(user: User): Boolean {
-        return user.capabilities?.spreedCapability?.features?.contains("temp-user-avatar-api") == true
-    }
-
-    fun isConversationAvatarEndpointAvailable(user: User): Boolean {
-        return user.capabilities?.spreedCapability?.features?.contains("avatar") == true
-    }
-
-    fun isConversationDescriptionEndpointAvailable(user: User): Boolean {
-        return user.capabilities?.spreedCapability?.features?.contains("room-description") == true
-    }
-
-    fun canEditScopes(user: User): Boolean {
-        return user.capabilities?.provisioningCapability?.accountPropertyScopesVersion != null &&
-            user.capabilities!!.provisioningCapability!!.accountPropertyScopesVersion!! > 1
-    }
-
-    fun isAbleToCall(user: User?): Boolean {
-        if (user?.capabilities != null) {
-            val capabilities = user.capabilities
-            return if (
-                capabilities?.spreedCapability?.config?.containsKey("call") == true &&
-                capabilities.spreedCapability!!.config!!["call"] != null &&
-                capabilities.spreedCapability!!.config!!["call"]!!.containsKey("enabled")
-            ) {
-                java.lang.Boolean.parseBoolean(capabilities.spreedCapability!!.config!!["call"]!!["enabled"].toString())
-            } else {
-                // older nextcloud versions without the capability can't disable the calls
-                true
-            }
-        }
-        return false
-    }
-
-    fun isCallReactionsSupported(user: User?): Boolean {
-        if (user?.capabilities != null) {
-            val capabilities = user.capabilities
-            return capabilities?.spreedCapability?.config?.containsKey("call") == true &&
-                capabilities.spreedCapability!!.config!!["call"] != null &&
-                capabilities.spreedCapability!!.config!!["call"]!!.containsKey("supported-reactions")
-        }
-        return false
-    }
-
-    @JvmStatic
-    fun isUnifiedSearchAvailable(user: User): Boolean {
-        return hasSpreedFeatureCapability(user, "unified-search")
-    }
-
-    @JvmStatic
-    fun isLinkPreviewAvailable(user: User): Boolean {
-        return user.capabilities?.coreCapability?.referenceApi != null &&
-            user.capabilities?.coreCapability?.referenceApi == "true"
-    }
-
-    fun isTranslationsSupported(user: User?): Boolean {
-        if (user?.capabilities != null) {
-            val capabilities = user.capabilities
-            return capabilities?.spreedCapability?.config?.containsKey("chat") == true &&
-                capabilities.spreedCapability!!.config!!["chat"] != null &&
-                capabilities.spreedCapability!!.config!!["chat"]!!.containsKey("has-translation-providers") &&
-                capabilities.spreedCapability!!.config!!["chat"]!!["has-translation-providers"] == true
-        }
-
-        return false
-    }
-
-    fun isRemindSupported(user: User?): Boolean {
-        if (user?.capabilities != null) {
-            val capabilities = user.capabilities
-            return capabilities?.spreedCapability?.features?.contains("remind-me-later") == true
-        }
-
-        return false
-    }
-
-    fun getRecordingConsentType(user: User?): Int {
-        if (user?.capabilities != null) {
-            val capabilities = user.capabilities
-            if (
-                capabilities?.spreedCapability?.config?.containsKey("call") == true &&
-                capabilities.spreedCapability!!.config!!["call"] != null &&
-                capabilities.spreedCapability!!.config!!["call"]!!.containsKey("recording-consent")
-            ) {
-                return when (
-                    capabilities.spreedCapability!!.config!!["call"]!!["recording-consent"].toString()
-                        .toInt()
-                ) {
-                    1 -> RECORDING_CONSENT_REQUIRED
-                    2 -> RECORDING_CONSENT_DEPEND_ON_CONVERSATION
-                    else -> RECORDING_CONSENT_NOT_REQUIRED
-                }
-            }
-        }
-        return RECORDING_CONSENT_NOT_REQUIRED
-    }
-
-    const val DEFAULT_CHAT_SIZE = 1000
-    const val RECORDING_CONSENT_NOT_REQUIRED = 0
-    const val RECORDING_CONSENT_REQUIRED = 1
-    const val RECORDING_CONSENT_DEPEND_ON_CONVERSATION = 2
-}

+ 4 - 3
app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java

@@ -35,7 +35,7 @@ import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.UserIdUtils;
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
+import com.nextcloud.talk.utils.CapabilitiesUtil;
 
 import javax.inject.Inject;
 
@@ -158,7 +158,8 @@ public class DatabaseStorageModule {
                 });
 
         } else if ("conversation_info_message_notifications_dropdown".equals(key)) {
-            if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "notification-levels")) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser.getCapabilities().getSpreedCapability(), "notification" +
+                "-levels")) {
                 if (TextUtils.isEmpty(messageNotificationLevel) || !messageNotificationLevel.equals(value)) {
                     int intValue;
                     switch (value) {
@@ -175,7 +176,7 @@ public class DatabaseStorageModule {
                             intValue = 0;
                     }
 
-                    int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
+                    int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.API_V4, 1});
 
                     ncApi.setNotificationLevel(ApiUtils.getCredentials(conversationUser.getUsername(),
                                                                        conversationUser.getToken()),

+ 3 - 4
app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt

@@ -25,7 +25,6 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import com.nextcloud.talk.activities.CallActivity.Companion.TAG
-import com.nextcloud.talk.location.GeocodingActivity
 import fr.dudie.nominatim.client.TalkJsonNominatimClient
 import fr.dudie.nominatim.model.Address
 import kotlinx.coroutines.CoroutineScope
@@ -70,9 +69,9 @@ class GeoCodingViewModel : ViewModel() {
                 try {
                     val results = nominatimClient.search(query) as ArrayList<Address>
                     for (address in results) {
-                        Log.d(GeocodingActivity.TAG, address.displayName)
-                        Log.d(GeocodingActivity.TAG, address.latitude.toString())
-                        Log.d(GeocodingActivity.TAG, address.longitude.toString())
+                        Log.d(TAG, address.displayName)
+                        Log.d(TAG, address.latitude.toString())
+                        Log.d(TAG, address.longitude.toString())
                     }
                     geocodingResults = results
                     geocodingResultsLiveData.postValue(results)

+ 1 - 1
app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java

@@ -113,7 +113,7 @@ public class WebSocketConnectionHelper {
     }
 
     HelloOverallWebSocketMessage getAssembledHelloModel(User user, String ticket) {
-        int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[]{ApiUtils.APIv3, 2, 1});
+        int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[]{ApiUtils.API_V3, 2, 1});
 
         HelloOverallWebSocketMessage helloOverallWebSocketMessage = new HelloOverallWebSocketMessage();
         helloOverallWebSocketMessage.setType("hello");

+ 3 - 3
app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt

@@ -22,7 +22,7 @@
 
 package com.nextcloud.talk.utils
 
-import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.conversations.Conversation
 import junit.framework.TestCase
 import org.junit.Test
@@ -31,7 +31,7 @@ class ParticipantPermissionsTest : TestCase() {
 
     @Test
     fun test_areFlagsSet() {
-        val user = User()
+        val spreedCapability = SpreedCapability()
         val conversation = Conversation()
         conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or
             ParticipantPermissions.JOIN_CALL or
@@ -39,7 +39,7 @@ class ParticipantPermissionsTest : TestCase() {
 
         val attendeePermissions =
             ParticipantPermissions(
-                user,
+                spreedCapability,
                 conversation
             )
 

+ 4 - 4
app/src/test/java/com/nextcloud/talk/utils/ShareUtilsTest.kt

@@ -23,7 +23,7 @@ import android.content.Context
 import android.content.res.Resources
 import com.nextcloud.talk.R
 import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.users.UserManager
 import io.reactivex.Maybe
 import org.junit.Assert
@@ -49,19 +49,19 @@ class ShareUtilsTest {
     private val baseUrl = "https://my.nextcloud.com"
     private val token = "2aotbrjr"
 
-    private lateinit var conversation: Conversation
+    private lateinit var conversation: ConversationModel
 
     @Before
     fun setUp() {
         MockitoAnnotations.openMocks(this)
         Mockito.`when`(userManager!!.currentUser).thenReturn(Maybe.just(user))
-        Mockito.`when`(user!!.baseUrl).thenReturn(baseUrl)
+        Mockito.`when`(user!!.baseUrl!!).thenReturn(baseUrl)
         Mockito.`when`(context!!.resources).thenReturn(resources)
         Mockito.`when`(resources!!.getString(R.string.nc_share_text))
             .thenReturn("Join the conversation at %1\$s/index.php/call/%2\$s")
         Mockito.`when`(resources.getString(R.string.nc_share_text_pass)).thenReturn("\nPassword: %1\$s")
 
-        conversation = Conversation(token = token)
+        conversation = ConversationModel(token = token)
     }
 
     @Test

+ 1 - 1
detekt.yml

@@ -1,5 +1,5 @@
 build:
-  maxIssues: 116
+  maxIssues: 122
   weights:
     # complexity: 2
     # LongParameterList: 1

+ 14 - 54
gradle/verification-keyring.keys

@@ -1313,8 +1313,6 @@ lQyC8nl8P5PgkEZ5CHcGymZlpzihR3ECrPJTk39Sb7D3SxCW4WrChV3kVfmLgvc=
 -----END PGP PUBLIC KEY BLOCK-----
 
 pub    C21CE653B639E41A
-uid    Eric Kuck <eric@bluelinelabs.com>
-
 sub    4F80368F9034B8D0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
 Version: BCPG v1.68
@@ -1324,21 +1322,20 @@ aBF7dud1bzw7voZo5ieGK923wUB+R9vQYd5DYfNLBHj9/TrTVCfKfUIeeEQRZYBz
 ufYcDwi4uVx9VPj2wRhkK+lzxphvosJCNFK8Vn82oY7eHQ1RA4AEhCeE/hz8maq6
 NPoOPjpEN0DVnPIYdjPsdqd4UKQzkX/wMOxghz8SdcVROzUoL+9pZzx968OFuGrV
 lUD0su37S6To1IUn6WNEuy1uJTzT3Zqi0hfm31AqPxlLWDOwnuKvUJl3RObyli2k
-CBtDa5xPE6GU6ZUEFUZ7qbk7iV5p8uTchKVbABEBAAG0IUVyaWMgS3VjayA8ZXJp
-Y0BibHVlbGluZWxhYnMuY29tPrkBDQRU5ChoAQgAxC44rEZjgnJevvrzdL5vCVqC
-1WI9cZ7L8DwF/pvm7NbRKC5GgXigul18UET80q4E3WIi0tTMG+pVWO+1v0dEu/Re
-B/l+hc76iwJjOlwSiQ1jvq6/q0Nhne0/0khSYNWyd0AwJ2VZktcD93dJV4EqTm1O
-Ck1gigAd/GN5wslQkMST/nUYUGm4cA/RZVSA8PSFZDZ2CxHyRyHgaOQNBUmWG2gf
-ExUrrJA26iKowkNqZXWegnzYwlf8ZRE6MZM0JPLOUw/+r4ybI8ny+/U55s2sm0XZ
-CcJvNda5N3SoaC/OgGWZFx1s9UksN7MmvhznaSUMeaeVFbGC3nu9dsQhV9RxMwAR
-AQABiQEfBBgBAgAJBQJU5ChoAhsMAAoJEMIc5lO2OeQadSwH/07x1foZKkFRGMlj
-wCmofKGSqZ9fu6ueOIV6fwHjrhlfkSyKN+96xbjhwIvWhKdSmWP/AsUqRDD/mTHw
-ZMdlgmdXkGEvvCuJDL5FlQzl9OWeeplfVhLysx6dzj/G8AUXlfEIGBvb8Q56d5dK
-MpId4H4vt+YIzS8x/ry+QTTDJAOu1cVJfwoX34yMcZ+IHTzly2XKi4zQ41DyfrgY
-lCodWna10RtBdPZY41Jf4xSezX2q7KZBXXRgyVNYu3dDuNzhJAJ6jy7eMcb6urK6
-n73cz5uZPmWIbp4cAecZB0BfMj/PW37dK0oYdWKLxaDwpxvIV7T45Y64Md2FCC+d
-nC2Xh7c=
-=kzY9
+CBtDa5xPE6GU6ZUEFUZ7qbk7iV5p8uTchKVbABEBAAG5AQ0EVOQoaAEIAMQuOKxG
+Y4JyXr7683S+bwlagtViPXGey/A8Bf6b5uzW0SguRoF4oLpdfFBE/NKuBN1iItLU
+zBvqVVjvtb9HRLv0Xgf5foXO+osCYzpcEokNY76uv6tDYZ3tP9JIUmDVsndAMCdl
+WZLXA/d3SVeBKk5tTgpNYIoAHfxjecLJUJDEk/51GFBpuHAP0WVUgPD0hWQ2dgsR
+8kch4GjkDQVJlhtoHxMVK6yQNuoiqMJDamV1noJ82MJX/GUROjGTNCTyzlMP/q+M
+myPJ8vv1OebNrJtF2QnCbzXWuTd0qGgvzoBlmRcdbPVJLDezJr4c52klDHmnlRWx
+gt57vXbEIVfUcTMAEQEAAYkBHwQYAQIACQUCVOQoaAIbDAAKCRDCHOZTtjnkGnUs
+B/9O8dX6GSpBURjJY8ApqHyhkqmfX7urnjiFen8B464ZX5EsijfvesW44cCL1oSn
+Uplj/wLFKkQw/5kx8GTHZYJnV5BhL7wriQy+RZUM5fTlnnqZX1YS8rMenc4/xvAF
+F5XxCBgb2/EOeneXSjKSHeB+L7fmCM0vMf68vkE0wyQDrtXFSX8KF9+MjHGfiB08
+5ctlyouM0ONQ8n64GJQqHVp2tdEbQXT2WONSX+MUns19quymQV10YMlTWLt3Q7jc
+4SQCeo8u3jHG+rqyup+93M+bmT5liG6eHAHnGQdAXzI/z1t+3StKGHVii8Wg8Kcb
+yFe0+OWOuDHdhQgvnZwtl4e3
+=DwNF
 -----END PGP PUBLIC KEY BLOCK-----
 
 pub    C488A74FCAE540C6
@@ -1674,43 +1671,6 @@ fW1AkBVEk6siyL8PXfxmj9ev3H9xiQVLyJ6HpdHTLVjHjFkgNOLd
 =R7zg
 -----END PGP PUBLIC KEY BLOCK-----
 
-pub    D041CAD2E452550F
-uid    Deanna <deannagarcia@google.com>
-
-sub    5199F3DAE89C332D
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.68
-
-mQGNBGCtdhoBDADdopjDt4eUNEqLJSw1ZICSR0oq09SOVtJSaSYdF8UiXjBfL1Ds
-fhTDqSv5pT2a2gLj0OU3tFhWHvINLaKKCjQnHVcFXi2LTxt+XBOjRYkFjHVisbaZ
-PZ6HnTMStPrvs+hQ168vU3VfYOsOLN22j53I/Ba+FA7E0G0bqkratuT5L7BTR1mC
-fqDaeisWSCllfe6EEysaFF+/1RcRy+Yt+8ZWV0FZEF7UwQvqKHcYmlkqPIn3v/8y
-J/yvmzIEtCQ1F+bvJbzaROmeJf254G2Uh7IfMYEm9WlqnGwNdbIhil7bdxq8Y/0H
-XbQPaESxkki7yL5JTfH/+UzdklMe+Dga273L/cgzfjV3zJJ9vR94W5ABAbGYh4ZW
-aKvNnT1m4vTbEMfo4r3NF2zc+K9Ly/JNaHqkR5M4SVElvN2lsC5KNUiRvExhg+h0
-mKyx61mu3gUIrC1UOmqhtx7RzQQf7ESMdzmNHY0P93lR0Ic10fyli0wfl7A6q7+q
-zV2a1V2k9Yg6B9sAEQEAAbQgRGVhbm5hIDxkZWFubmFnYXJjaWFAZ29vZ2xlLmNv
-bT65AY0EYK12GgEMAMgP3//QeBsTS3IrfSp3m44el96X6BWona2yo4DvVyuwqfUL
-ZE+Nhj7I+kEZLrA29AOySOD/6quJ4MIJZfq/Do920Di8/10WQ00OdCM1wH7bMz2U
-vcSqsr0iOgQtycuUf7JOHSTME9vqk+C3Lhn0r59AVaRdXEe6zBgNZyzZJeCr5F8w
-RhglPlwvhOGs2aLEqlCxFnY4pLayQFoQyw1lDjHIXHg5JtfOHvqiNXVDcGpyKLG8
-SzImp62iL4sfuA0weVIQeS9kZiQabSYKvSf3TvNXYTgmFz/vjPbYhv9LTkBroTlV
-g3l+UmAxLrHVuXMx0zX3jfNNHAqUjVhPYZhnifMkmGJgLeMIVqr5Q/tx8pzyYiiO
-cqQ1zDg8ubJDGRue1JjlUGdw19OvhFDs+lydukt8Mmhb0gPkBLi2syZHgYHtEooX
-PLwEsJ+SynZCFhZiWj8BsWNFJpaDd8ynNeWhMAcwi3B5ZeQiZaAlV0sItxsrzvbu
-4ZYZtkjAkQdsaaTWSwARAQABiQG8BBgBCgAmFiEEaWthmaKp2MKc54zA0EHK0uRS
-VQ8FAmCtdhoCGwwFCQPCZwAACgkQ0EHK0uRSVQ+G7wwAvaVPDgnM+i2pGQPwq6Mk
-SzhKEG4H1pvBWyYR8H9D3p/dE33IjVu3EEy1h37Nzdyp46KtASGNe3KBodSsh6gv
-PlV5pNGxMNbX6fo8ZGtS83C+6uTF1cYmuO1nmi8P4+7qtcNZg4xv/ujAZIC20kem
-YKDth3FvPxEXsoxY+Ns7sxgd3SqoyLhjcyoczI8uyhim5nfvvbnEd6WrdiBPBtb/
-F1h/nfqdFj2TcZkAlnzGnlVlgU8J60u6zE+9VvBm0lJR73Ar55mQEwarGFPL1a3/
-A7ZEeNa0Dc3Oa5sKMYtxMlGKZ0WGUoGcDWiaDEsv5YyRnaSOaXKM1NkJCR013QAr
-RcHrRBPo+0/RIZVE+b8oEcmGzdL8HNwnm7e06ruZryF9LQA5YBmCKE0urigmgEvC
-zZsj/fMJ+OIZcAhE7UVae48GpW2kLATxmK01oSzvizIlmN3rVz2EnjOun2iuuEpF
-/lmDbjK5n1r3f8npB1l1fT5cozzQJkPVYzhBWH1KXP5X
-=nh9O
------END PGP PUBLIC KEY BLOCK-----
-
 pub    D364ABAA39A47320
 sub    3F606403DCA455C8
 -----BEGIN PGP PUBLIC KEY BLOCK-----

Some files were not shown because too many files changed in this diff