소스 검색

Merge pull request #3663 from nextcloud/feature/noid/handleFederationRoomCapabilities

Feature/noid/handle federation room capabilities
Marcel Hibbe 1 년 전
부모
커밋
f11b426421
100개의 변경된 파일2084개의 추가작업 그리고 1554개의 파일을 삭제
  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-----

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.