diff --git a/CHANGES.md b/CHANGES.md index 60e2f41125..1849bde397 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,22 @@ +Changes in Element v1.1.9 (2021-06-02) +====================================== + +Features ✨: + - Upgrade Olm dependency to 3.2.4 + - Allow user to add custom "network" in room search (#1458) + - Add Gitter.im as a default in the Change Network menu (#3196) + - VoIP: support for virtual rooms (#3355) + - Compress thumbnail: change Jpeg quality from 100 to 80 (#3396) + - Inconsistent usage of the term homeserver in Settings (#3404) + - VoIP: support attended transfer (#3420) + - /snow -> /snowfall and update wording (iso Element Web) (#3430) + +Bugfixes 🐛: + - Fix | On Android it seems to be impossible to view the complete description of a Space (without dev tools) (#3401) + - Fix | Suggest Rooms, Show a detailed view of the room on click (#3406) + - Fix app crashing when signing out (#3424) + - Switch to stable endpoint/fields for MSC2858 (#3442) + Changes in Element 1.1.8 (2021-05-25) =================================================== diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 6935e1d46b..0f3817d983 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -53,7 +53,7 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.core:core-ktx:1.5.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.recyclerview:recyclerview:1.2.0" diff --git a/fastlane/metadata/android/en-US/changelogs/40101090.txt b/fastlane/metadata/android/en-US/changelogs/40101090.txt new file mode 100644 index 0000000000..a5a4e14a29 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40101090.txt @@ -0,0 +1,2 @@ +Main changes in this version: add support for gitter.im network. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.9 \ No newline at end of file diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 67a35cac2e..252359193d 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo @@ -177,10 +177,10 @@ class RxSession(private val session: Session) { } } - fun liveAccountData(types: Set): Observable> { - return session.getLiveAccountDataEvents(types).asObservable() + fun liveUserAccountData(types: Set): Observable> { + return session.userAccountDataService().getLiveAccountDataEvents(types).asObservable() .startWithCallable { - session.getAccountDataEvents(types) + session.userAccountDataService().getAccountDataEvents(types) } } @@ -201,8 +201,8 @@ class RxSession(private val session: Session) { } fun liveSecretSynchronisationInfo(): Observable { - return Observable.combineLatest, Optional, Optional, SecretsSynchronisationInfo>( - liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)), + return Observable.combineLatest, Optional, Optional, SecretsSynchronisationInfo>( + liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)), liveCrossSigningInfo(session.myUserId), liveCrossSigningPrivateKeys(), Function3 { _, crossSigningInfo, pInfo -> diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 99c43ce144..875662759a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -112,7 +112,7 @@ dependencies { def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' - def daggerVersion = '2.35.1' + def daggerVersion = '2.36' def work_version = '2.5.0' def retrofit_version = '2.9.0' @@ -121,7 +121,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.appcompat:appcompat:1.3.0" - implementation "androidx.core:core-ktx:1.3.2" + implementation "androidx.core:core-ktx:1.5.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" @@ -155,7 +155,7 @@ dependencies { implementation "io.arrow-kt:arrow-instances-core:$arrow_version" // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm - implementation 'org.matrix.gitlab.matrix-org:olm:3.2.2' + implementation 'org.matrix.gitlab.matrix-org:olm:3.2.4' // DI implementation "com.google.dagger:dagger:$daggerVersion" @@ -169,7 +169,7 @@ dependencies { implementation 'com.otaliastudios:transcoder:0.10.3' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.24' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index eb4773f3c8..25c22bca57 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -33,7 +33,7 @@ import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -73,12 +73,12 @@ class QuadSTests : InstrumentedTest { // Assert Account data is updated val accountDataLock = CountDownLatch(1) - var accountData: UserAccountDataEvent? = null + var accountData: AccountDataEvent? = null val liveAccountData = runBlocking(Dispatchers.Main) { - aliceSession.getLiveAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") + aliceSession.userAccountDataService().getLiveAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") } - val accountDataObserver = Observer?> { t -> + val accountDataObserver = Observer?> { t -> if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { accountData = t.getOrNull() accountDataLock.countDown() @@ -100,13 +100,13 @@ class QuadSTests : InstrumentedTest { quadS.setDefaultKey(TEST_KEY_ID) } - var defaultKeyAccountData: UserAccountDataEvent? = null + var defaultKeyAccountData: AccountDataEvent? = null val defaultDataLock = CountDownLatch(1) val liveDefAccountData = runBlocking(Dispatchers.Main) { - aliceSession.getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + aliceSession.userAccountDataService().getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } - val accountDefDataObserver = Observer?> { t -> + val accountDefDataObserver = Observer?> { t -> if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { defaultKeyAccountData = t.getOrNull()!! defaultDataLock.countDown() @@ -206,7 +206,7 @@ class QuadSTests : InstrumentedTest { ) } - val accountDataEvent = aliceSession.getAccountDataEvent("my.secret") + val accountDataEvent = aliceSession.userAccountDataService().getAccountDataEvent("my.secret") val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> assertEquals("Content should contains two encryptions", 2, encryptedContent?.keys?.size ?: 0) @@ -275,14 +275,14 @@ class QuadSTests : InstrumentedTest { mTestHelper.signOutAndClose(aliceSession) } - private fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + private fun assertAccountData(session: Session, type: String): AccountDataEvent { val accountDataLock = CountDownLatch(1) - var accountData: UserAccountDataEvent? = null + var accountData: AccountDataEvent? = null val liveAccountData = runBlocking(Dispatchers.Main) { - session.getLiveAccountDataEvent(type) + session.userAccountDataService().getLiveAccountDataEvent(type) } - val accountDataObserver = Observer?> { t -> + val accountDataObserver = Observer?> { t -> if (t?.getOrNull()?.type == type) { accountData = t.getOrNull() accountDataLock.countDown() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index 64b3e180aa..a0733dda97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -51,12 +51,12 @@ data class SsoIdentityProvider( ) : Parcelable, Comparable { companion object { - const val BRAND_GOOGLE = "org.matrix.google" - const val BRAND_GITHUB = "org.matrix.github" - const val BRAND_APPLE = "org.matrix.apple" - const val BRAND_FACEBOOK = "org.matrix.facebook" - const val BRAND_TWITTER = "org.matrix.twitter" - const val BRAND_GITLAB = "org.matrix.gitlab" + const val BRAND_GOOGLE = "google" + const val BRAND_GITHUB = "github" + const val BRAND_APPLE = "apple" + const val BRAND_FACEBOOK = "facebook" + const val BRAND_TWITTER = "twitter" + const val BRAND_GITLAB = "gitlab" } override fun compareTo(other: SsoIdentityProvider): Int { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index b5f90e87ea..e888e5d2de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -78,7 +78,6 @@ interface Session : InitialSyncProgressService, HomeServerCapabilitiesService, SecureStorageService, - AccountDataService, AccountService { /** @@ -239,6 +238,11 @@ interface Session : */ fun openIdService(): OpenIdService + /** + * Returns the user account data service associated with the session + */ + fun userAccountDataService(): AccountDataService + /** * Add a listener to the session. * @param listener the listener to add. @@ -262,12 +266,17 @@ interface Session : * A global session listener to get notified for some events. */ interface Listener : SessionLifecycleObserver { + /** + * Called when the session received new invites to room so the client can react to it once. + */ + fun onNewInvitedRoom(session: Session, roomId: String) = Unit + /** * Possible cases: * - The access token is not valid anymore, * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver */ - fun onGlobalError(session: Session, globalError: GlobalError) + fun onGlobalError(session: Session, globalError: GlobalError) = Unit } val sharedSecretStorageService: SharedSecretStorageService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataEvent.kt similarity index 97% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataEvent.kt index 744e3e5379..e5cbd07aaf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataEvent.kt @@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.Content * Currently used types are defined in [UserAccountDataTypes]. */ @JsonClass(generateAdapter = true) -data class UserAccountDataEvent( +data class AccountDataEvent( @Json(name = "type") val type: String, @Json(name = "content") val content: Content ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt index 5ebeaad3de..77f3eb0cd9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/AccountDataService.kt @@ -20,28 +20,31 @@ import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.util.Optional +/** + * This service can be attached globally to the session so it represents user data or attached to a single room. + */ interface AccountDataService { /** * Retrieve the account data with the provided type or null if not found */ - fun getAccountDataEvent(type: String): UserAccountDataEvent? + fun getAccountDataEvent(type: String): AccountDataEvent? /** * Observe the account data with the provided type */ - fun getLiveAccountDataEvent(type: String): LiveData> + fun getLiveAccountDataEvent(type: String): LiveData> /** * Retrieve the account data with the provided types. The return list can have a different size that * the size of the types set, because some AccountData may not exist. * If an empty set is provided, all the AccountData are retrieved */ - fun getAccountDataEvents(types: Set): List + fun getAccountDataEvents(types: Set): List /** * Observe the account data with the provided types. If an empty set is provided, all the AccountData are observed */ - fun getLiveAccountDataEvents(types: Set): LiveData> + fun getLiveAccountDataEvents(types: Set): LiveData> /** * Update the account data with the provided type and the provided account data content diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt new file mode 100644 index 0000000000..43e6872525 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import java.util.UUID + +object CallIdGenerator { + fun generate() = UUID.randomUUID().toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index dc67aa536a..c34744e75f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -20,8 +20,6 @@ interface CallSignalingService { suspend fun getTurnServer(): TurnServerResponse - fun getPSTNProtocolChecker(): PSTNProtocolChecker - /** * Create an outgoing call */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 7533619eb0..fcc9f7072d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -26,8 +26,12 @@ interface MxCallDetail { val callId: String val isOutgoing: Boolean val roomId: String - val opponentUserId: String val isVideoCall: Boolean + val ourPartyId: String + val opponentPartyId: Optional? + val opponentVersion: Int + val opponentUserId: String + val capabilities: CallCapabilities? } /** @@ -39,12 +43,6 @@ interface MxCall : MxCallDetail { const val VOIP_PROTO_VERSION = 1 } - val ourPartyId: String - var opponentPartyId: Optional? - var opponentVersion: Int - - var capabilities: CallCapabilities? - var state: CallState /** @@ -91,8 +89,12 @@ interface MxCall : MxCallDetail { /** * Send a m.call.replaces event to initiate call transfer. + * See [org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent] for documentation about the parameters */ - suspend fun transfer(targetUserId: String, targetRoomId: String?) + suspend fun transfer(targetUserId: String, + targetRoomId: String?, + createCallId: String?, + awaitCallId: String?) fun addListener(listener: StateListener) fun removeListener(listener: StateListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt deleted file mode 100644 index 6627f62e24..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.api.session.call - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.thirdparty.GetThirdPartyProtocolsTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject - -private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn" -private const val PSTN_MATRIX_KEY = "m.protocol.pstn" - -/** - * This class is responsible for checking if the HS support the PSTN protocol. - * As long as the request succeed, it'll check only once by session. - */ -@SessionScope -class PSTNProtocolChecker @Inject internal constructor(private val taskExecutor: TaskExecutor, - private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask) { - - interface Listener { - fun onPSTNSupportUpdated() - } - - private var alreadyChecked = AtomicBoolean(false) - - private val pstnSupportListeners = mutableListOf() - - fun addListener(listener: Listener) { - pstnSupportListeners.add(listener) - } - - fun removeListener(listener: Listener) { - pstnSupportListeners.remove(listener) - } - - var supportedPSTNProtocol: String? = null - private set - - fun checkForPSTNSupportIfNeeded() { - if (alreadyChecked.get()) return - taskExecutor.executorScope.checkForPSTNSupport() - } - - private fun CoroutineScope.checkForPSTNSupport() = launch { - try { - supportedPSTNProtocol = getSupportedPSTN(3) - alreadyChecked.set(true) - if (supportedPSTNProtocol != null) { - pstnSupportListeners.forEach { - tryOrNull { it.onPSTNSupportUpdated() } - } - } - } catch (failure: Throwable) { - Timber.v("Fail to get supported PSTN, will check again next time.") - } - } - - private suspend fun getSupportedPSTN(maxTries: Int): String? { - val thirdPartyProtocols: Map = try { - getThirdPartyProtocolsTask.execute(Unit) - } catch (failure: Throwable) { - if (maxTries == 1) { - throw failure - } else { - // Wait for 10s before trying again - delay(10_000L) - return getSupportedPSTN(maxTries - 1) - } - } - return when { - thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY - thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY - else -> null - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 03381cd61b..229a53fa9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -31,13 +31,10 @@ object EventType { const val TYPING = "m.typing" const val REDACTION = "m.room.redaction" const val RECEIPT = "m.receipt" - const val TAG = "m.tag" const val ROOM_KEY = "m.room_key" - const val FULLY_READ = "m.fully_read" const val PLUMBING = "m.room.plumbing" const val BOT_OPTIONS = "m.room.bot.options" const val PREVIEW_URLS = "org.matrix.room.preview_urls" - const val MARKED_UNREAD = "com.famedly.marked_unread" // State Events diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index ac1d726d03..a6d4583c76 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -54,7 +54,7 @@ interface PermalinkService { * * @return the permalink, or null in case of error */ - fun createRoomPermalink(roomId: String): String? + fun createRoomPermalink(roomId: String, viaServers: List? = null): String? /** * Creates a permalink for an event. If you have an event you can use [createPermalink] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 8c434fc440..41bcbf8ff6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService @@ -55,7 +56,8 @@ interface Room : RoomCallService, RelationService, RoomCryptoService, - RoomPushRuleService { + RoomPushRuleService, + AccountDataService { /** * The roomId of this room @@ -86,12 +88,12 @@ interface Room : * @return The search result */ suspend fun search(searchTerm: String, - nextBatch: String?, - orderByRecent: Boolean, - limit: Int, - beforeLimit: Int, - afterLimit: Int, - includeProfile: Boolean): SearchResult + nextBatch: String?, + orderByRecent: Boolean, + limit: Int, + beforeLimit: Int, + afterLimit: Int, + includeProfile: Boolean): SearchResult /** * Use this room as a Space, if the type is correct. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt new file mode 100644 index 0000000000..9dcf1306d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.accountdata + +object RoomAccountDataTypes { + const val EVENT_TYPE_VIRTUAL_ROOM = "im.vector.is_virtual_room" + const val EVENT_TYPE_TAG = "m.tag" + const val EVENT_TYPE_FULLY_READ = "m.fully_read" + const val MARKED_UNREAD = "com.famedly.marked_unread" +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 66293bcb8c..8cd2a0538d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -32,5 +32,7 @@ data class SpaceChildInfo( val parentRoomId: String?, val suggested: Boolean?, val canonicalAlias: String?, - val aliases: List? + val aliases: List?, + val worldReadable: Boolean + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt index 45a54bb379..180b32db05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -44,7 +44,7 @@ data class CallAnswerContent( * Capability advertisement. */ @Json(name = "capabilities") val capabilities: CallCapabilities? = null -): CallSignallingContent { +): CallSignalingContent { @JsonClass(generateAdapter = true) data class Answer( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt index 7bfe7a97ac..dc0a1e3b2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -41,4 +41,4 @@ data class CallCandidatesContent( * Required. The version of the VoIP specification this messages adheres to. */ @Json(name = "version") override val version: String? -): CallSignallingContent +): CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index 0acc409053..9d6e1a7eae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -44,7 +44,7 @@ data class CallHangupContent( * One of: ["ice_failed", "invite_timeout"] */ @Json(name = "reason") val reason: Reason? = null -) : CallSignallingContent { +) : CallSignalingContent { @JsonClass(generateAdapter = false) enum class Reason { @Json(name = "ice_failed") @@ -56,6 +56,9 @@ data class CallHangupContent( @Json(name = "user_hangup") USER_HANGUP, + @Json(name = "replaced") + REPLACED, + @Json(name = "user_media_failed") USER_MEDIA_FAILED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt index 42489bc0ce..e4332f0ea7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -55,7 +55,7 @@ data class CallInviteContent( */ @Json(name = "capabilities") val capabilities: CallCapabilities? = null -): CallSignallingContent { +): CallSignalingContent { @JsonClass(generateAdapter = true) data class Offer( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt index 040993bb51..68dd5ef043 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt @@ -47,7 +47,7 @@ data class CallNegotiateContent( */ @Json(name = "version") override val version: String? - ): CallSignallingContent { + ): CallSignalingContent { @JsonClass(generateAdapter = true) data class Description( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt index 1da229b179..ea412fbe3e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt @@ -37,4 +37,4 @@ data class CallRejectContent( * Required. The version of the VoIP specification this message adheres to. */ @Json(name = "version") override val version: String? -) : CallSignallingContent +) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 97a3b8c7a7..4559c5db6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -38,30 +38,30 @@ data class CallReplacesContent( */ @Json(name = "replacement_id") val replacementId: String? = null, /** - * Optional. If specified, the transferee client waits for an invite to this room and joins it - * (possibly waiting for user confirmation) and then continues the transfer in this room. - * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. + * Optional. If specified, the transferee client waits for an invite to this room and joins it + * (possibly waiting for user confirmation) and then continues the transfer in this room. + * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. */ - @Json(name = "target_room") val targerRoomId: String? = null, + @Json(name = "target_room") val targetRoomId: String? = null, /** - * An object giving information about the transfer target + * An object giving information about the transfer target */ @Json(name = "target_user") val targetUser: TargetUser? = null, /** - * If specified, gives the call ID for the transferee's client to use when placing the replacement call. - * Mutually exclusive with await_call + * If specified, gives the call ID for the transferee's client to use when placing the replacement call. + * Mutually exclusive with await_call */ @Json(name = "create_call") val createCall: String? = null, /** - * If specified, gives the call ID that the transferee's client should wait for. - * Mutually exclusive with create_call. + * If specified, gives the call ID that the transferee's client should wait for. + * Mutually exclusive with create_call. */ @Json(name = "await_call") val awaitCall: String? = null, /** * Required. The version of the VoIP specification this messages adheres to. */ @Json(name = "version") override val version: String? -): CallSignallingContent { +): CallSignalingContent { @JsonClass(generateAdapter = true) data class TargetUser( @@ -77,6 +77,5 @@ data class CallReplacesContent( * Optional. The avatar URL of the transfer target. */ @Json(name = "avatar_url") val avatarUrl: String? - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt index 6ea70ac990..795f332711 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt @@ -41,4 +41,4 @@ data class CallSelectAnswerContent( * Required. The version of the VoIP specification this message adheres to. */ @Json(name = "version") override val version: String? -): CallSignallingContent +): CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignalingContent.kt similarity index 96% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignalingContent.kt index f8d8c2a5e8..92b43dd22c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignalingContent.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.api.session.room.model.call -interface CallSignallingContent { +interface CallSignalingContent { /** * Required. A unique identifier for the call. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index c96a800ee5..1e8959afc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -35,5 +35,5 @@ object MessageType { const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" const val MSGTYPE_CONFETTI = "nic.custom.confetti" - const val MSGTYPE_SNOW = "io.element.effect.snowfall" + const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index 888950dc12..b78cd5e032 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -28,7 +28,8 @@ sealed class PeekResult { val numJoinedMembers: Int?, val roomType: String?, val viaServers: List, - val someMembers: List? + val someMembers: List?, + val isPublic: Boolean ) : PeekResult() data class PeekingNotAllowed( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt index d77dfcfe35..246813a524 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt @@ -22,16 +22,16 @@ import org.matrix.android.sdk.api.util.JsonDict @JsonClass(generateAdapter = true) data class ThirdPartyUser( - /* - Required. A Matrix User ID represting a third party user. + /** + * Required. A Matrix User ID representing a third party user. */ @Json(name = "userid") val userId: String, - /* - Required. The protocol ID that the third party location is a part of. + /** + * Required. The protocol ID that the third party location is a part of. */ @Json(name = "protocol") val protocol: String, - /* - Required. Information used to identify this third party location. + /** + * Required. Information used to identify this third party location. */ @Json(name = "fields") val fields: JsonDict ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index deb279eb95..3d2773fb4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -38,6 +39,8 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } data class EventItem(override val id: String, @@ -47,6 +50,8 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } data class RoomItem(override val id: String, @@ -56,6 +61,19 @@ sealed class MatrixItem( init { if (BuildConfig.DEBUG) checkId() } + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) + } + + data class SpaceItem(override val id: String, + override val displayName: String? = null, + override val avatarUrl: String? = null) + : MatrixItem(id, displayName, avatarUrl) { + init { + if (BuildConfig.DEBUG) checkId() + } + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } data class RoomAliasItem(override val id: String, @@ -68,6 +86,8 @@ sealed class MatrixItem( // Best name is the id, and we keep the displayName of the room for the case we need the first letter override fun getBestName() = id + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } data class GroupItem(override val id: String, @@ -80,6 +100,8 @@ sealed class MatrixItem( // Best name is the id, and we keep the displayName of the room for the case we need the first letter override fun getBestName() = id + + override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar) } open fun getBestName(): String { @@ -92,12 +114,15 @@ sealed class MatrixItem( } } + abstract fun updateAvatar(newAvatar: String?): MatrixItem + /** * Return the prefix as defined in the matrix spec (and not extracted from the id) */ fun getIdPrefix() = when (this) { is UserItem -> '@' is EventItem -> '$' + is SpaceItem, is RoomItem -> '!' is RoomAliasItem -> '#' is GroupItem -> '+' @@ -148,7 +173,11 @@ fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) -fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) +fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) { + MatrixItem.SpaceItem(roomId, displayName, avatarUrl) +} else { + MatrixItem.RoomItem(roomId, displayName, avatarUrl) +} fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) @@ -159,4 +188,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) -fun SpaceChildInfo.toMatrixItem() = MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias ?: "", avatarUrl) +fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) { + MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl) +} else { + MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias, avatarUrl) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index e0c52cf9ca..3742a429d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -33,7 +33,6 @@ internal const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login */ internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" -internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 46256f4b81..20ce438d8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -88,11 +88,9 @@ internal class DefaultAuthenticationService @Inject constructor( return buildString { append(homeServerUrlBase) + append(SSO_REDIRECT_PATH) if (providerId != null) { - append(MSC2858_SSO_REDIRECT_PATH) append("/$providerId") - } else { - append(SSO_REDIRECT_PATH) } // Set the redirect url appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index d0d17e2cd5..c718fae390 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -42,7 +42,7 @@ internal data class LoginFlow( * the client can show a button for each of the supported providers * See MSC #2858 */ - @Json(name = "org.matrix.msc2858.identity_providers") + @Json(name = "identity_providers") val ssoIdentityProvider: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index ef607de103..2d989b58e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm import io.realm.FieldAttribute import io.realm.RealmMigration +import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent @@ -30,6 +31,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields @@ -49,7 +51,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { const val SESSION_STORE_SCHEMA_SC_VERSION = 2L const val SESSION_STORE_SCHEMA_SC_VERSION_OFFSET = (1L shl 12) - const val SESSION_STORE_SCHEMA_VERSION = 13L + + const val SESSION_STORE_SCHEMA_VERSION = 14L + SESSION_STORE_SCHEMA_SC_VERSION * SESSION_STORE_SCHEMA_SC_VERSION_OFFSET } @@ -72,6 +74,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 10) migrateTo11(realm) if (oldVersion <= 11) migrateTo12(realm) if (oldVersion <= 12) migrateTo13(realm) + if (oldVersion <= 13) migrateTo14(realm) if (oldScVersion <= 0) migrateToSc1(realm) if (oldScVersion <= 1) migrateToSc2(realm) @@ -306,11 +309,29 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { private fun migrateTo13(realm: DynamicRealm) { Timber.d("Step 12 -> 13") - // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12() realm.schema.get("SpaceChildSummaryEntity") ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) } ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) } + + private fun migrateTo14(realm: DynamicRealm) { + Timber.d("Step 13 -> 14") + val roomAccountDataSchema = realm.schema.create("RoomAccountDataEntity") + .addField(RoomAccountDataEntityFields.CONTENT_STR, String::class.java) + .addField(RoomAccountDataEntityFields.TYPE, String::class.java, FieldAttribute.INDEXED) + + realm.schema.get("RoomEntity") + ?.addRealmListField(RoomEntityFields.ACCOUNT_DATA.`$`, roomAccountDataSchema) + + realm.schema.get("RoomSummaryEntity") + ?.addField(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, Boolean::class.java, FieldAttribute.INDEXED) + ?.transform { + val isHiddenFromUser = it.getString(RoomSummaryEntityFields.VERSIONING_STATE_STR) == VersioningState.UPGRADED_ROOM_JOINED.name + it.setBoolean(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, isHiddenFromUser) + } + + roomAccountDataSchema.isEmbedded = true + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt index 54315a1835..4edfdad897 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/AccountDataMapper.kt @@ -17,17 +17,25 @@ package org.matrix.android.sdk.internal.database.mapper import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import javax.inject.Inject internal class AccountDataMapper @Inject constructor(moshi: Moshi) { private val adapter = moshi.adapter>(JSON_DICT_PARAMETERIZED_TYPE) - fun map(entity: UserAccountDataEntity): UserAccountDataEvent { - return UserAccountDataEvent( + fun map(entity: UserAccountDataEntity): AccountDataEvent { + return AccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() + ) + } + + fun map(entity: RoomAccountDataEntity): AccountDataEvent { + return AccountDataEvent( type = entity.type ?: "", content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 06b4ccefb4..c5a9e51785 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.mapper +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo @@ -104,7 +105,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa parentRoomId = roomSummaryEntity.roomId, suggested = it.suggested, canonicalAlias = it.childSummaryEntity?.canonicalAlias, - aliases = it.childSummaryEntity?.aliases?.toList() + aliases = it.childSummaryEntity?.aliases?.toList(), + worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC ) }, flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt new file mode 100644 index 0000000000..40040b5738 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomAccountDataEntity.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.Index +import io.realm.annotations.RealmClass + +@RealmClass(embedded = true) +internal open class RoomAccountDataEntity( + @Index var type: String? = null, + var contentStr: String? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 58297776f0..65483e05bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -23,7 +23,8 @@ import io.realm.annotations.PrimaryKey internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), - var sendingTimelineEvents: RealmList = RealmList() + var sendingTimelineEvents: RealmList = RealmList(), + var accountData: RealmList = RealmList() ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index e76e3358d0..b68be14496 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -268,6 +268,12 @@ internal open class RoomSummaryEntity( } } + @Index + var isHiddenFromUser: Boolean = false + set(value) { + if (value != field) field = value + } + @Index private var versioningStateStr: String = VersioningState.NONE.name var versioningState: VersioningState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 72ae512fa5..19472e21d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -62,6 +62,7 @@ import io.realm.annotations.RealmModule UserAccountDataEntity::class, ScalarTokenEntity::class, WellknownIntegrationManagerConfigEntity::class, + RoomAccountDataEntity::class, SpaceChildSummaryEntity::class, SpaceParentSummaryEntity::class ]) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt index 27e8d9d8d1..a551f97379 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomEntityQueries.kt @@ -29,6 +29,10 @@ internal fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuer .equalTo(RoomEntityFields.ROOM_ID, roomId) } +internal fun RoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomEntity { + return where(realm, roomId).findFirst() ?: realm.createObject(RoomEntity::class.java, roomId) +} + internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { val query = realm.where() if (membership != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/GlobalErrorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/GlobalErrorHandler.kt index 9afdb40ed1..8be11e80f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/GlobalErrorHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/GlobalErrorHandler.kt @@ -16,13 +16,13 @@ package org.matrix.android.sdk.internal.network +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.TaskExecutor -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -44,7 +44,6 @@ internal class GlobalErrorHandler @Inject constructor( sessionParamsStore.setTokenInvalid(sessionId) } } - listener?.onGlobalError(globalError) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index b100a336a7..1f47978198 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -74,6 +74,7 @@ import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataService import org.matrix.android.sdk.internal.util.createUIHandler import timber.log.Timber import javax.inject.Inject @@ -117,7 +118,7 @@ internal class DefaultSession @Inject constructor( private val contentDownloadStateTracker: ContentDownloadStateTracker, private val initialSyncProgressService: Lazy, private val homeServerCapabilitiesService: Lazy, - private val accountDataService: Lazy, + private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, private val eventService: Lazy, @@ -130,6 +131,7 @@ internal class DefaultSession @Inject constructor( @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, + GlobalErrorHandler.Listener, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), GroupService by groupService.get(), @@ -144,9 +146,7 @@ internal class DefaultSession @Inject constructor( SecureStorageService by secureStorageService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), ProfileService by profileService.get(), - AccountDataService by accountDataService.get(), - AccountService by accountService.get(), - GlobalErrorHandler.Listener { + AccountService by accountService.get() { override val sharedSecretStorageService: SharedSecretStorageService get() = _sharedSecretStorageService.get() @@ -164,16 +164,16 @@ internal class DefaultSession @Inject constructor( override fun open() { assert(!isOpen) isOpen = true + globalErrorHandler.listener = this cryptoService.get().ensureDevice() uiHandler.post { lifecycleObservers.forEach { it.onSessionStarted(this) } - sessionListeners.dispatch { - it.onSessionStarted(this) + sessionListeners.dispatch { _, listener -> + listener.onSessionStarted(this) } } - globalErrorHandler.listener = this } override fun requireBackgroundSync() { @@ -213,13 +213,13 @@ internal class DefaultSession @Inject constructor( // timelineEventDecryptor.destroy() uiHandler.post { lifecycleObservers.forEach { it.onSessionStopped(this) } - sessionListeners.dispatch { - it.onSessionStopped(this) + sessionListeners.dispatch { _, listener -> + listener.onSessionStopped(this) } } cryptoService.get().close() - isOpen = false globalErrorHandler.listener = null + isOpen = false } override fun getSyncStateLive() = getSyncThread().liveState() @@ -243,8 +243,8 @@ internal class DefaultSession @Inject constructor( lifecycleObservers.forEach { it.onClearCache(this) } - sessionListeners.dispatch { - it.onClearCache(this) + sessionListeners.dispatch { _, listener -> + listener.onClearCache(this) } } withContext(NonCancellable) { @@ -254,8 +254,8 @@ internal class DefaultSession @Inject constructor( } override fun onGlobalError(globalError: GlobalError) { - sessionListeners.dispatch { - it.onGlobalError(this, globalError) + sessionListeners.dispatch { _, listener -> + listener.onGlobalError(this, globalError) } } @@ -293,6 +293,8 @@ internal class DefaultSession @Inject constructor( override fun openIdService(): OpenIdService = openIdService.get() + override fun userAccountDataService(): AccountDataService = accountDataService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt index 563ff4ada3..d5c661b1e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt @@ -16,10 +16,17 @@ package org.matrix.android.sdk.internal.session +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.SessionManager +import org.matrix.android.sdk.internal.di.SessionId +import timber.log.Timber import javax.inject.Inject -internal class SessionListeners @Inject constructor() { +@SessionScope +internal class SessionListeners @Inject constructor( + @SessionId private val sessionId: String, + private val sessionManager: SessionManager) { private val listeners = mutableSetOf() @@ -35,11 +42,18 @@ internal class SessionListeners @Inject constructor() { } } - fun dispatch(block: (Session.Listener) -> Unit) { + fun dispatch(block: (Session, Session.Listener) -> Unit) { synchronized(listeners) { + val session = getSession() ?: return Unit.also { + Timber.w("You don't have any attached session") + } listeners.forEach { - block(it) + tryOrNull { block(session, it) } } } } + + private fun getSession(): Session? { + return sessionManager.getSessionComponent(sessionId)?.session() + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index de74b34818..49ce92372e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -93,7 +93,7 @@ import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProces import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker -import org.matrix.android.sdk.internal.session.user.accountdata.DefaultAccountDataService +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataService import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter import org.matrix.android.sdk.internal.util.md5 import retrofit2.Retrofit @@ -364,7 +364,7 @@ internal abstract class SessionModule { abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService @Binds - abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService + abstract fun bindAccountDataService(service: UserAccountDataService): AccountDataService @Binds abstract fun bindEventService(service: DefaultEventService): EventService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 8d7e9e819a..61ea660b60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -24,18 +24,15 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent -import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent -import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent -import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @SessionScope @@ -192,6 +189,9 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa // Ignore remote echo return } + if (event.roomId == null || event.senderId == null) { + return + } if (event.senderId == userId) { // discard current call, it's answered by another of my session activeCallHandler.removeCall(call.callId) @@ -201,20 +201,16 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}") return } - call.apply { - opponentPartyId = Optional.from(content.partyId) - opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION - capabilities = content.capabilities ?: CallCapabilities() - } + mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities) callListenersDispatcher.onCallAnswerReceived(content) } } - private fun MxCall.partyIdsMatches(contentSignallingContent: CallSignallingContent): Boolean { - return opponentPartyId?.getOrNull() == contentSignallingContent.partyId + private fun MxCall.partyIdsMatches(contentSignalingContent: CallSignalingContent): Boolean { + return opponentPartyId?.getOrNull() == contentSignalingContent.partyId } - private fun CallSignallingContent.getCall(): MxCall? { + private fun CallSignalingContent.getCall(): MxCall? { val currentCall = callId?.let { activeCallHandler.getCallWithId(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 7d046cb642..da1f84cc89 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.call import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.MxCall -import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber @@ -30,18 +29,13 @@ internal class DefaultCallSignalingService @Inject constructor( private val callSignalingHandler: CallSignalingHandler, private val mxCallFactory: MxCallFactory, private val activeCallHandler: ActiveCallHandler, - private val turnServerDataSource: TurnServerDataSource, - private val pstnProtocolChecker: PSTNProtocolChecker + private val turnServerDataSource: TurnServerDataSource ) : CallSignalingService { override suspend fun getTurnServer(): TurnServerResponse { return turnServerDataSource.getTurnServer() } - override fun getPSTNProtocolChecker(): PSTNProtocolChecker { - return pstnProtocolChecker - } - override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { return mxCallFactory.createOutgoingCall(roomId, otherUserId, isVideoCall).also { activeCallHandler.addCall(it) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index b14cdca63c..547be2253f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -17,18 +17,17 @@ package org.matrix.android.sdk.internal.session.call import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.call.CallIdGenerator import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent -import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import java.math.BigDecimal -import java.util.UUID import javax.inject.Inject internal class MxCallFactory @Inject constructor( @@ -48,32 +47,38 @@ internal class MxCallFactory @Inject constructor( roomId = roomId, userId = userId, ourPartyId = deviceId ?: "", - opponentUserId = opponentUserId, isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, getProfileInfoTask = getProfileInfoTask ).apply { - opponentPartyId = Optional.from(content.partyId) - opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION - capabilities = content.capabilities ?: CallCapabilities() + updateOpponentData(opponentUserId, content, content.capabilities) } } fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall { return MxCallImpl( - callId = UUID.randomUUID().toString(), + callId = CallIdGenerator.generate(), isOutgoing = true, roomId = roomId, userId = userId, ourPartyId = deviceId ?: "", - opponentUserId = opponentUserId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, getProfileInfoTask = getProfileInfoTask - ) + ).apply { + // Setup with this userId, might be updated when processing the Answer event + this.opponentUserId = opponentUserId + } + } + + fun updateOutgoingCallWithOpponentData(call: MxCall, + userId: String, + content: CallSignalingContent, + callCapabilities: CallCapabilities?) { + (call as? MxCallImpl)?.updateOpponentData(userId, content, callCapabilities) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 88fba0ea85..f101685a4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.call.model import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.call.CallIdGenerator import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.events.model.Content @@ -36,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService @@ -43,14 +45,13 @@ import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import timber.log.Timber -import java.util.UUID +import java.math.BigDecimal internal class MxCallImpl( override val callId: String, override val isOutgoing: Boolean, override val roomId: String, private val userId: String, - override val opponentUserId: String, override val isVideoCall: Boolean, override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, @@ -61,8 +62,16 @@ internal class MxCallImpl( override var opponentPartyId: Optional? = null override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override lateinit var opponentUserId: String override var capabilities: CallCapabilities? = null + fun updateOpponentData(userId: String, content: CallSignalingContent, callCapabilities: CallCapabilities?) { + opponentPartyId = Optional.from(content.partyId) + opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + opponentUserId = userId + capabilities = callCapabilities ?: CallCapabilities() + } + override var state: CallState = CallState.Idle set(value) { field = value @@ -202,7 +211,10 @@ internal class MxCallImpl( .also { eventSenderProcessor.postEvent(it) } } - override suspend fun transfer(targetUserId: String, targetRoomId: String?) { + override suspend fun transfer(targetUserId: String, + targetRoomId: String?, + createCallId: String?, + awaitCallId: String?) { val profileInfoParams = GetProfileInfoTask.Params(targetUserId) val profileInfo = try { getProfileInfoTask.execute(profileInfoParams) @@ -213,15 +225,16 @@ internal class MxCallImpl( CallReplacesContent( callId = callId, partyId = ourPartyId, - replacementId = UUID.randomUUID().toString(), + replacementId = CallIdGenerator.generate(), version = MxCall.VOIP_PROTO_VERSION.toString(), targetUser = CallReplacesContent.TargetUser( id = targetUserId, displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String, avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String ), - targerRoomId = targetRoomId, - createCall = UUID.randomUUID().toString() + targetRoomId = targetRoomId, + awaitCall = awaitCallId, + createCall = createCallId ) .let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index c28668a53e..82cd682eae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -23,8 +23,11 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.util.MimeTypes import timber.log.Timber import java.io.ByteArrayOutputStream +import javax.inject.Inject -internal object ThumbnailExtractor { +internal class ThumbnailExtractor @Inject constructor( + private val context: Context +) { class ThumbnailData( val width: Int, @@ -34,22 +37,22 @@ internal object ThumbnailExtractor { val mimeType: String ) - fun extractThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { return if (attachment.type == ContentAttachmentData.Type.VIDEO) { - extractVideoThumbnail(context, attachment) + extractVideoThumbnail(attachment) } else { null } } - private fun extractVideoThumbnail(context: Context, attachment: ContentAttachmentData): ThumbnailData? { + private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { var thumbnailData: ThumbnailData? = null val mediaMetadataRetriever = MediaMetadataRetriever() try { mediaMetadataRetriever.setDataSource(context, attachment.queryUri) mediaMetadataRetriever.frameAtTime?.let { thumbnail -> val outputStream = ByteArrayOutputStream() - thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + thumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) val thumbnailWidth = thumbnail.width val thumbnailHeight = thumbnail.height val thumbnailSize = outputStream.size() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 06cbf1ba90..237411db53 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -82,6 +82,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var imageCompressor: ImageCompressor @Inject lateinit var videoCompressor: VideoCompressor + @Inject lateinit var thumbnailExtractor: ThumbnailExtractor @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var temporaryFileCreator: TemporaryFileCreator @@ -302,7 +303,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter * If appropriate, it will create and upload a thumbnail */ private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? { - return ThumbnailExtractor.extractThumbnail(context, params.attachment) + return thumbnailExtractor.extractThumbnail(params.attachment) ?.let { thumbnailData -> val thumbnailProgressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index 475781ef01..4f88d8eb95 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -44,7 +44,7 @@ import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureProtocol @@ -77,7 +77,7 @@ internal class DefaultIdentityService @Inject constructor( private val submitTokenForBindingTask: IdentitySubmitTokenForBindingTask, private val unbindThreePidsTask: UnbindThreePidsTask, private val identityApiProvider: IdentityApiProvider, - private val accountDataDataSource: AccountDataDataSource, + private val accountDataDataSource: UserAccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, private val sessionParams: SessionParams ) : IdentityService, SessionLifecycleObserver { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index 3df9a00cc1..f79f8084a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -33,8 +33,8 @@ import org.matrix.android.sdk.internal.extensions.observeNotNull import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence @@ -57,7 +57,7 @@ import javax.inject.Inject internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration, @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val accountDataDataSource: AccountDataDataSource, + private val accountDataDataSource: UserAccountDataDataSource, private val widgetFactory: WidgetFactory) : SessionLifecycleObserver { @@ -240,7 +240,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri ) } - private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { + private fun AccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? { return extractWidgetSequence(widgetFactory) .filter { WidgetType.IntegrationManager == it.type diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt index 7db9d8f68a..134da4ce51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt @@ -33,8 +33,8 @@ internal class DefaultPermalinkService @Inject constructor( return permalinkFactory.createPermalink(id) } - override fun createRoomPermalink(roomId: String): String? { - return permalinkFactory.createRoomPermalink(roomId) + override fun createRoomPermalink(roomId: String, viaServers: List?): String? { + return permalinkFactory.createRoomPermalink(roomId, viaServers) } override fun createPermalink(roomId: String, eventId: String): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt index 970752449a..639e45582a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt @@ -40,11 +40,18 @@ internal class PermalinkFactory @Inject constructor( } else MATRIX_TO_URL_BASE + escape(id) } - fun createRoomPermalink(roomId: String): String? { + fun createRoomPermalink(roomId: String, via: List? = null): String? { return if (roomId.isEmpty()) { null } else { - MATRIX_TO_URL_BASE + escape(roomId) + viaParameterFinder.computeViaParams(userId, roomId) + buildString { + append(MATRIX_TO_URL_BASE) + append(escape(roomId)) + append( + via?.takeIf { it.isNotEmpty() }?.let { viaParameterFinder.asUrlViaParameters(it) } + ?: viaParameterFinder.computeViaParams(userId, roomId) + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index 0da60e9ba2..72fbfcced5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -39,8 +39,11 @@ internal class ViaParameterFinder @Inject constructor( * current user one. */ fun computeViaParams(userId: String, roomId: String): String { - return computeViaParams(userId, roomId, 3) - .joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } + return asUrlViaParameters(computeViaParams(userId, roomId, 3)) + } + + fun asUrlViaParameters(viaList: List): String { + return viaList.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } } fun computeViaParams(userId: String, roomId: String, max: Int): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index a5e066dae8..5a2eef7e8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.Room @@ -41,34 +42,35 @@ import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder +import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataService import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask import org.matrix.android.sdk.internal.session.space.DefaultSpace import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException -import javax.inject.Inject -internal class DefaultRoom @Inject constructor(override val roomId: String, - private val roomSummaryDataSource: RoomSummaryDataSource, - private val timelineService: TimelineService, - private val sendService: SendService, - private val draftService: DraftService, - private val stateService: StateService, - private val uploadsService: UploadsService, - private val reportingService: ReportingService, - private val roomCallService: RoomCallService, - private val readService: ReadService, - private val typingService: TypingService, - private val aliasService: AliasService, - private val tagsService: TagsService, - private val cryptoService: CryptoService, - private val relationService: RelationService, - private val roomMembersService: MembershipService, - private val roomPushRuleService: RoomPushRuleService, - private val sendStateTask: SendStateTask, - private val viaParameterFinder: ViaParameterFinder, - private val searchTask: SearchTask) : +internal class DefaultRoom(override val roomId: String, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val timelineService: TimelineService, + private val sendService: SendService, + private val draftService: DraftService, + private val stateService: StateService, + private val uploadsService: UploadsService, + private val reportingService: ReportingService, + private val roomCallService: RoomCallService, + private val readService: ReadService, + private val typingService: TypingService, + private val aliasService: AliasService, + private val tagsService: TagsService, + private val cryptoService: CryptoService, + private val relationService: RelationService, + private val roomMembersService: MembershipService, + private val roomPushRuleService: RoomPushRuleService, + private val roomAccountDataService: RoomAccountDataService, + private val sendStateTask: SendStateTask, + private val viaParameterFinder: ViaParameterFinder, + private val searchTask: SearchTask) : Room, TimelineService by timelineService, SendService by sendService, @@ -83,7 +85,8 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, TagsService by tagsService, RelationService by relationService, MembershipService by roomMembersService, - RoomPushRuleService by roomPushRuleService { + RoomPushRuleService by roomPushRuleService, + AccountDataService by roomAccountDataService { override fun getRoomSummaryLive(): LiveData> { return roomSummaryDataSource.getRoomSummaryLive(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 6fee630510..4f12604039 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -360,4 +360,13 @@ internal interface RoomAPI { suspend fun deleteTag(@Path("userId") userId: String, @Path("roomId") roomId: String, @Path("tag") tag: String) + + /** + * Set an AccountData event to the room. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/account_data/{type}") + suspend fun setRoomAccountData(@Path("userId") userId: String, + @Path("roomId") roomId: String, + @Path("type") type: String, + @Body content: JsonDict) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 3f743c2922..8efbf2360a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataService import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService @@ -60,6 +61,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory, + private val roomAccountDataServiceFactory: RoomAccountDataService.Factory, private val sendStateTask: SendStateTask, private val viaParameterFinder: ViaParameterFinder, private val searchTask: SearchTask) : @@ -84,6 +86,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: relationService = relationServiceFactory.create(roomId), roomMembersService = membershipServiceFactory.create(roomId), roomPushRuleService = roomPushRuleServiceFactory.create(roomId), + roomAccountDataService = roomAccountDataServiceFactory.create(roomId), sendStateTask = sendStateTask, searchTask = searchTask, viaParameterFinder = viaParameterFinder diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index e70235a273..12bd0d2e74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.session.room.accountdata.DefaultUpdateRoomAccountDataTask +import org.matrix.android.sdk.internal.session.room.accountdata.UpdateRoomAccountDataTask import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultAddRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultDeleteRoomAliasTask @@ -241,6 +243,9 @@ internal abstract class RoomModule { @Binds abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask + @Binds + abstract fun bindUpdateRoomAccountDataTask(task: DefaultUpdateRoomAccountDataTask): UpdateRoomAccountDataTask + @Binds abstract fun bindGetEventTask(task: DefaultGetEventTask): GetEventTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt new file mode 100644 index 0000000000..0bcf9d7f38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.accountdata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import javax.inject.Inject + +internal class RoomAccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(roomId: String, type: String): AccountDataEvent? { + return getAccountDataEvents(roomId, setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(roomId: String, type: String): LiveData> { + return Transformations.map(getLiveAccountDataEvents(roomId, setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(roomId: String, types: Set): List { + return realmSessionProvider.withRealm { realm -> + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return@withRealm emptyList() + roomEntity.accountDataEvents(types) + } + } + + fun getLiveAccountDataEvents(roomId: String, types: Set): LiveData> { + val liveRoomEntity = monarchy.findAllManagedWithChanges { RoomEntity.where(it, roomId) } + val resultLiveData = MediatorLiveData>() + resultLiveData.addSource(liveRoomEntity) { + val roomEntity = it.realmResults.firstOrNull() + if (roomEntity == null) { + resultLiveData.postValue(emptyList()) + } else { + val mappedResult = roomEntity.accountDataEvents(types) + resultLiveData.postValue(mappedResult) + } + } + return resultLiveData + } + + private fun RoomEntity.accountDataEvents(types: Set): List { + val query = accountData.where() + if (types.isNotEmpty()) { + query.`in`(RoomAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query.findAll().map { accountDataMapper.map(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataService.kt new file mode 100644 index 0000000000..9e9e9dc322 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataService.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.accountdata + +import androidx.lifecycle.LiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataService +import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Optional + +internal class RoomAccountDataService @AssistedInject constructor(@Assisted private val roomId: String, + private val dataSource: RoomAccountDataDataSource, + private val updateRoomAccountDataTask: UpdateRoomAccountDataTask +) : AccountDataService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): RoomAccountDataService + } + + override fun getAccountDataEvent(type: String): AccountDataEvent? { + return dataSource.getAccountDataEvent(roomId, type) + } + + override fun getLiveAccountDataEvent(type: String): LiveData> { + return dataSource.getLiveAccountDataEvent(roomId, type) + } + + override fun getAccountDataEvents(types: Set): List { + return dataSource.getAccountDataEvents(roomId, types) + } + + override fun getLiveAccountDataEvents(types: Set): LiveData> { + return dataSource.getLiveAccountDataEvents(roomId, types) + } + + override suspend fun updateAccountData(type: String, content: Content) { + val params = UpdateRoomAccountDataTask.Params(roomId, type, content) + return updateRoomAccountDataTask.execute(params) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/UpdateRoomAccountDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/UpdateRoomAccountDataTask.kt new file mode 100644 index 0000000000..db18c18908 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/UpdateRoomAccountDataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.accountdata + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface UpdateRoomAccountDataTask : Task { + + data class Params( + val roomId: String, + val type: String, + val content: JsonDict + ) +} + +internal class DefaultUpdateRoomAccountDataTask @Inject constructor( + private val roomApi: RoomAPI, + @UserId private val userId: String, + private val globalErrorReceiver: GlobalErrorReceiver +) : UpdateRoomAccountDataTask { + + override suspend fun execute(params: UpdateRoomAccountDataTask.Params) { + return executeRequest(globalErrorReceiver) { + roomApi.setRoomAccountData(userId, params.roomId, params.type, params.content) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt index 95572c203c..cc66a0a2d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -37,6 +37,7 @@ internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveP val predecessorRoomSummary = RoomSummaryEntity.where(realm, predecessorRoomId).findFirst() ?: RoomSummaryEntity(predecessorRoomId) predecessorRoomSummary.versioningState = VersioningState.UPGRADED_ROOM_JOINED + predecessorRoomSummary.isHiddenFromUser = true realm.insertOrUpdate(predecessorRoomSummary) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt index c6f4bbb4e1..219e9c903f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -23,6 +23,8 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent @@ -105,7 +107,8 @@ internal class DefaultPeekRoomTask @Inject constructor( numJoinedMembers = publicRepoResult.numJoinedMembers, viaServers = serverList, roomType = null, // would be nice to get that from directory... - someMembers = null + someMembers = null, + isPublic = true ) } @@ -143,6 +146,11 @@ internal class DefaultPeekRoomTask @Inject constructor( } } + val historyVisibility = + stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY && it.stateKey?.isNotEmpty() == true } + ?.let { it.content?.toModel()?.historyVisibility } + val roomType = stateEvents .lastOrNull { it.type == EventType.STATE_ROOM_CREATE } ?.content @@ -158,7 +166,8 @@ internal class DefaultPeekRoomTask @Inject constructor( numJoinedMembers = memberCount, roomType = roomType, viaServers = serverList, - someMembers = someMembers + someMembers = someMembers, + isPublic = historyVisibility == RoomHistoryVisibility.WORLD_READABLE ) } catch (failure: Throwable) { // Would be M_FORBIDDEN if cannot peek :/ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt index 075c2b50a3..ce05c923b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.internal.database.query.isMarkedUnread import org.matrix.android.sdk.internal.session.sync.RoomMarkedUnreadHandler import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI @@ -55,7 +56,7 @@ internal class DefaultSetMarkedUnreadTask @Inject constructor( if (isMarkedUnread(monarchy.realmConfiguration, params.roomId) != params.markedUnread) { updateDatabase(params.roomId, params.markedUnread) executeRequest(globalErrorReceiver, canRetry = true) { - accountDataApi.setRoomAccountData(userId, params.roomId, EventType.MARKED_UNREAD, params.markedUnreadContent) + accountDataApi.setRoomAccountData(userId, params.roomId, RoomAccountDataTypes.MARKED_UNREAD, params.markedUnreadContent) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index c1ad6205c3..f505b13b33 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -73,6 +73,7 @@ internal class LocalEchoEventFactory @Inject constructor( @UserId private val userId: String, private val markdownParser: MarkdownParser, private val textPillsUtils: TextPillsUtils, + private val thumbnailExtractor: ThumbnailExtractor, private val localEchoRepository: LocalEchoRepository, private val permalinkFactory: PermalinkFactory ) { @@ -261,7 +262,7 @@ internal class LocalEchoEventFactory @Inject constructor( val width = firstFrame?.width ?: 0 mediaDataRetriever.release() - val thumbnailInfo = ThumbnailExtractor.extractThumbnail(context, attachment)?.let { + val thumbnailInfo = thumbnailExtractor.extractThumbnail(attachment)?.let { ThumbnailInfo( width = it.width, height = it.height, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 456290ca16..dbde169e31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -36,7 +36,6 @@ import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType -import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount @@ -250,7 +249,7 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) - query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + query.equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false) queryParams.roomCategoryFilter?.let { when (it) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index dad3296e20..ede7c4b1ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.createObject import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent @@ -28,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.EventDecryptor @@ -55,10 +57,10 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver +import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo -import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber @@ -71,7 +73,7 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val stateEventDataSource: StateEventDataSource) { + private val roomAccountDataDataSource: RoomAccountDataDataSource) { fun update(realm: Realm, roomId: String, @@ -100,6 +102,10 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.membership = membership } + // Hard to filter from the app now we use PagedList... + roomSummaryEntity.isHiddenFromUser = roomSummaryEntity.versioningState == VersioningState.UPGRADED_ROOM_JOINED + || roomAccountDataDataSource.getAccountDataEvent(roomId, RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) != null + val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root @@ -311,7 +317,7 @@ internal class RoomSummaryUpdater @Inject constructor( // Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}") lookupMap.entries - .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN } + .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN } .forEach { entry -> val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst() if (parent != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index d0ad19245f..9c6153b349 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -147,7 +147,8 @@ internal class DefaultSpaceService @Inject constructor( parentRoomId = childStateEv.roomId, suggested = childStateEvContent.suggested, canonicalAlias = childSummary.canonicalAlias, - aliases = childSummary.aliases + aliases = childSummary.aliases, + worldReadable = childSummary.worldReadable ) } }.orEmpty() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 3e88f15573..c3586bcea7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -55,8 +54,6 @@ import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler -import org.matrix.android.sdk.internal.session.room.read.FullyReadContent -import org.matrix.android.sdk.internal.session.room.read.MarkedUnreadContent import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput @@ -64,17 +61,15 @@ import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.RoomSync -import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse +import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler import org.matrix.android.sdk.internal.util.computeBestChunkSize import timber.log.Timber import javax.inject.Inject internal class RoomSyncHandler @Inject constructor(private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, - private val roomTagHandler: RoomTagHandler, - private val roomFullyReadHandler: RoomFullyReadHandler, - private val roomMarkedUnreadHandler: RoomMarkedUnreadHandler, + private val roomAccountDataHandler: RoomSyncAccountDataHandler, private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler, @@ -200,11 +195,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle ?.takeIf { it.isNotEmpty() } ?.let { handleEphemeral(realm, roomId, it, insertType == EventInsertType.INITIAL_SYNC, aggregator) } - if (roomSync.accountData?.events?.isNotEmpty() == true) { - handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) + if (roomSync.accountData != null) { + roomAccountDataHandler.handle(realm, roomId, roomSync.accountData) } - val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + val roomEntity = RoomEntity.getOrCreate(realm, roomId) if (roomEntity.membership == Membership.INVITE) { roomEntity.chunks.deleteAllFromRealm() @@ -267,7 +262,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle insertType: EventInsertType, syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle invited sync for room $roomId") - val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + val roomEntity = RoomEntity.getOrCreate(realm, roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { roomSync.inviteState.events.forEach { event -> @@ -296,7 +291,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomSync: RoomSync, insertType: EventInsertType, syncLocalTimestampMillis: Long): RoomEntity { - val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + val roomEntity = RoomEntity.getOrCreate(realm, roomId) for (event in roomSync.state?.events.orEmpty()) { if (event.eventId == null || event.stateKey == null || event.type == null) { continue @@ -462,20 +457,4 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle return result } - - private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { - accountData.events?.forEach { event -> - val eventType = event.getClearType() - if (eventType == EventType.TAG) { - val content = event.getClearContent().toModel() - roomTagHandler.handle(realm, roomId, content) - } else if (eventType == EventType.FULLY_READ) { - val content = event.getClearContent().toModel() - roomFullyReadHandler.handle(realm, roomId, content) - } else if (eventType == EventType.MARKED_UNREAD) { - val content = event.getClearContent().toModel() - roomMarkedUnreadHandler.handle(realm, roomId, content) - } - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 157787c8cf..a4468a96c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionListeners import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.reportSubtask @@ -44,6 +45,7 @@ private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" internal class SyncResponseHandler @Inject constructor( @SessionDatabase private val monarchy: Monarchy, @SessionId private val sessionId: String, + private val sessionListeners: SessionListeners, private val workManagerProvider: WorkManagerProvider, private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, @@ -125,6 +127,7 @@ internal class SyncResponseHandler @Inject constructor( syncResponse.rooms?.let { checkPushRules(it, isInitialSync) userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite) + dispatchInvitedRoom(it) } syncResponse.groups?.let { scheduleGroupDataFetchingIfNeeded(it) @@ -139,6 +142,13 @@ internal class SyncResponseHandler @Inject constructor( } } + private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) { + roomsSyncResponse.invite.keys.forEach { roomId -> + sessionListeners.dispatch { session, listener -> + listener.onNewInvitedRoom(session, roomId) } + } + } + /** * At the moment we don't get any group data through the sync, so we poll where every hour. * You can also force to refetch group data using [Group] API. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt index b8d987d500..3aebd90ed2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt @@ -23,7 +23,7 @@ import io.realm.kotlin.where import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.pushrules.RuleSetKey import org.matrix.android.sdk.api.pushrules.rest.GetPushRulesResponse -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.toModel @@ -113,7 +113,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( } } - private fun handlePushRules(realm: Realm, event: UserAccountDataEvent) { + private fun handlePushRules(realm: Realm, event: AccountDataEvent) { val pushRules = event.content.toModel() ?: return realm.where(PushRulesEntity::class.java) .findAll() @@ -155,7 +155,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( realm.insertOrUpdate(underrides) } - private fun handleDirectChatRooms(realm: Realm, event: UserAccountDataEvent) { + private fun handleDirectChatRooms(realm: Realm, event: AccountDataEvent) { val content = event.content.toModel() ?: return content.forEach { (userId, roomIds) -> roomIds.forEach { roomId -> @@ -181,7 +181,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( } } - private fun handleIgnoredUsers(realm: Realm, event: UserAccountDataEvent) { + private fun handleIgnoredUsers(realm: Realm, event: AccountDataEvent) { val userIds = event.content.toModel()?.ignoredUsers?.keys ?: return realm.where(IgnoredUserEntity::class.java) .findAll() @@ -191,7 +191,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( // TODO If not initial sync, we should execute a init sync } - private fun handleBreadcrumbs(realm: Realm, event: UserAccountDataEvent) { + private fun handleBreadcrumbs(realm: Realm, event: AccountDataEvent) { val recentRoomIds = event.content.toModel()?.recentRoomIds ?: return val entity = BreadcrumbsEntity.getOrCreate(realm) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt index 05b50ab2c5..ddb71cd19f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/accountdata/UserAccountDataSync.kt @@ -18,9 +18,9 @@ package org.matrix.android.sdk.internal.session.sync.model.accountdata import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent @JsonClass(generateAdapter = true) internal data class UserAccountDataSync( - @Json(name = "events") val list: List = emptyList() + @Json(name = "events") val list: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt new file mode 100644 index 0000000000..8f035e45db --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.parsing + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes +import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity +import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.session.room.read.FullyReadContent +import org.matrix.android.sdk.internal.session.room.read.MarkedUnreadContent +import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler +import org.matrix.android.sdk.internal.session.sync.RoomMarkedUnreadHandler +import org.matrix.android.sdk.internal.session.sync.RoomTagHandler +import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData +import javax.inject.Inject + +internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler, + private val roomFullyReadHandler: RoomFullyReadHandler, + private val roomMarkedUnreadHandler: RoomMarkedUnreadHandler) { + + fun handle(realm: Realm, roomId: String, accountData: RoomSyncAccountData) { + if (accountData.events.isNullOrEmpty()) { + return + } + val roomEntity = RoomEntity.getOrCreate(realm, roomId) + for (event in accountData.events) { + val eventType = event.getClearType() + handleGeneric(roomEntity, event.getClearContent(), eventType) + if (eventType == RoomAccountDataTypes.EVENT_TYPE_TAG) { + val content = event.getClearContent().toModel() + roomTagHandler.handle(realm, roomId, content) + } else if (eventType == RoomAccountDataTypes.EVENT_TYPE_FULLY_READ) { + val content = event.getClearContent().toModel() + roomFullyReadHandler.handle(realm, roomId, content) + } else if (eventType == RoomAccountDataTypes.MARKED_UNREAD) { + val content = event.getClearContent().toModel() + roomMarkedUnreadHandler.handle(realm, roomId, content) + } + } + } + + private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { + val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() + if (existing != null) { + // Update current value + existing.contentStr = ContentMapper.map(content) + } else { + val roomAccountData = RoomAccountDataEntity( + type = eventType, + contentStr = ContentMapper.map(content) + ) + roomEntity.accountData.add(roomAccountData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index bac725fad2..2c7dc92ddd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -30,7 +30,7 @@ import org.matrix.android.sdk.internal.session.identity.IdentityAuthAPI import org.matrix.android.sdk.internal.session.identity.IdentityRegisterTask import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTermsContent -import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.util.ensureTrailingSlash import javax.inject.Inject @@ -38,7 +38,7 @@ import javax.inject.Inject internal class DefaultTermsService @Inject constructor( @UnauthenticatedWithCertificate private val unauthenticatedOkHttpClient: Lazy, - private val accountDataDataSource: AccountDataDataSource, + private val accountDataDataSource: UserAccountDataDataSource, private val termsAPI: TermsAPI, private val retrofitFactory: RetrofitFactory, private val getOpenIdTokenTask: GetOpenIdTokenTask, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 2e03bc7a86..3ecc39ac94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -38,7 +38,7 @@ internal interface ThirdPartyAPI { * * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/user/{protocol}") suspend fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map?): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt similarity index 79% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt index d145c008ba..f64b1bdd2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.RealmSessionProvider @@ -31,27 +31,27 @@ import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityField import org.matrix.android.sdk.internal.di.SessionDatabase import javax.inject.Inject -internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, - private val realmSessionProvider: RealmSessionProvider, - private val accountDataMapper: AccountDataMapper) { +internal class UserAccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, + private val accountDataMapper: AccountDataMapper) { - fun getAccountDataEvent(type: String): UserAccountDataEvent? { + fun getAccountDataEvent(type: String): AccountDataEvent? { return getAccountDataEvents(setOf(type)).firstOrNull() } - fun getLiveAccountDataEvent(type: String): LiveData> { + fun getLiveAccountDataEvent(type: String): LiveData> { return Transformations.map(getLiveAccountDataEvents(setOf(type))) { it.firstOrNull()?.toOptional() } } - fun getAccountDataEvents(types: Set): List { + fun getAccountDataEvents(types: Set): List { return realmSessionProvider.withRealm { accountDataEventsQuery(it, types).findAll().map(accountDataMapper::map) } } - fun getLiveAccountDataEvents(types: Set): LiveData> { + fun getLiveAccountDataEvents(types: Set): LiveData> { return monarchy.findAllMappedWithChanges( { accountDataEventsQuery(it, types) }, accountDataMapper::map diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataService.kt similarity index 87% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataService.kt index 27db30f3b3..b15d1d0f8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataService.kt @@ -23,33 +23,33 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.awaitCallback import javax.inject.Inject -internal class DefaultAccountDataService @Inject constructor( +internal class UserAccountDataService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, - private val accountDataDataSource: AccountDataDataSource, + private val accountDataDataSource: UserAccountDataDataSource, private val taskExecutor: TaskExecutor ) : AccountDataService { - override fun getAccountDataEvent(type: String): UserAccountDataEvent? { + override fun getAccountDataEvent(type: String): AccountDataEvent? { return accountDataDataSource.getAccountDataEvent(type) } - override fun getLiveAccountDataEvent(type: String): LiveData> { + override fun getLiveAccountDataEvent(type: String): LiveData> { return accountDataDataSource.getLiveAccountDataEvent(type) } - override fun getAccountDataEvents(types: Set): List { + override fun getAccountDataEvents(types: Set): List { return accountDataDataSource.getAccountDataEvents(types) } - override fun getLiveAccountDataEvents(types: Set): LiveData> { + override fun getLiveAccountDataEvents(types: Set): LiveData> { return accountDataDataSource.getLiveAccountDataEvents(types) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index d741dbc966..ca1a129da7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -39,7 +39,7 @@ import org.matrix.android.sdk.api.session.SessionLifecycleObserver import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource -import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource +import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence import java.util.HashMap @@ -47,7 +47,7 @@ import javax.inject.Inject @SessionScope internal class WidgetManager @Inject constructor(private val integrationManager: IntegrationManager, - private val accountDataDataSource: AccountDataDataSource, + private val accountDataDataSource: UserAccountDataDataSource, private val stateEventDataSource: StateEventDataSource, private val createWidgetTask: CreateWidgetTask, private val widgetFactory: WidgetFactory, @@ -150,8 +150,8 @@ internal class WidgetManager @Inject constructor(private val integrationManager: return widgetsAccountData.mapToWidgets(widgetTypes, excludedTypes) } - private fun UserAccountDataEvent.mapToWidgets(widgetTypes: Set? = null, - excludedTypes: Set? = null): List { + private fun AccountDataEvent.mapToWidgets(widgetTypes: Set? = null, + excludedTypes: Set? = null): List { return extractWidgetSequence(widgetFactory) .filter { val widgetType = it.widgetContent.type ?: return@filter false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt index 6f423b38a0..5aa32d5a31 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/UserAccountWidgets.kt @@ -19,10 +19,10 @@ package org.matrix.android.sdk.internal.session.widgets.helper import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.session.widgets.model.Widget -internal fun UserAccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { +internal fun AccountDataEvent.extractWidgetSequence(widgetFactory: WidgetFactory): Sequence { return content.asSequence() .mapNotNull { @Suppress("UNCHECKED_CAST") diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 25d8adfdc6..ecccc76ad8 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,7 +43,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.3.0' - implementation "androidx.fragment:fragment-ktx:1.3.3" + implementation "androidx.fragment:fragment-ktx:1.3.4" implementation 'androidx.exifinterface:exifinterface:1.3.2' // Log diff --git a/vector/build.gradle b/vector/build.gradle index 944f876c00..79431d57de 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -14,7 +14,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 1 -ext.versionPatch = 8 +ext.versionPatch = 9 ext.scVersion = 34 @@ -301,13 +301,13 @@ android { dependencies { def epoxy_version = '4.6.1' - def fragment_version = '1.3.3' + def fragment_version = '1.3.4' def arrow_version = "0.8.2" def markwon_version = '4.1.2' def big_image_viewer_version = '1.8.0' def glide_version = '4.12.0' def moshi_version = '1.12.0' - def daggerVersion = '2.35.1' + def daggerVersion = '2.36' def autofill_version = "1.1.0" def work_version = '2.5.0' def arch_version = '2.1.0' @@ -336,7 +336,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.1.0" - implementation 'androidx.core:core-ktx:1.5.0-rc01' + implementation 'androidx.core:core-ktx:1.5.0' implementation "androidx.media:media:1.3.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" @@ -354,7 +354,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.24' // rx implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 0a724b62c6..b5f45e6586 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -104,7 +104,7 @@ class DefaultErrorFormatter @Inject constructor( } } } - is Failure.OtherServerError -> { + is Failure.OtherServerError -> { when (throwable.httpCode) { HttpURLConnection.HTTP_NOT_FOUND -> // homeserver not found @@ -116,9 +116,9 @@ class DefaultErrorFormatter @Inject constructor( throwable.localizedMessage } } - is DialPadLookup.Failure -> + is DialPadLookup.Failure -> stringProvider.getString(R.string.call_dial_pad_lookup_error) - else -> throwable.localizedMessage + else -> throwable.localizedMessage } ?: stringProvider.getString(R.string.unknown_error) } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt index 55ec8b605e..de469b9e3a 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt @@ -94,6 +94,10 @@ fun AppCompatActivity.addFragmentToBackstack( } } +fun AppCompatActivity.popBackstack() { + supportFragmentManager.popBackStack() +} + fun AppCompatActivity.resetBackstack() { repeat(supportFragmentManager.backStackEntryCount) { supportFragmentManager.popBackStack() diff --git a/vector/src/main/java/im/vector/app/core/extensions/Session.kt b/vector/src/main/java/im/vector/app/core/extensions/Session.kt index a21d4ecab3..699247ab6d 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Session.kt @@ -32,6 +32,7 @@ fun Session.configureAndStart(context: Context) { setFilter(FilterService.FilterPreset.ElementFilter) startSyncing(context) refreshPushers() + context.vectorComponent().webRtcCallManager().checkForProtocolsSupportIfNeeded() } fun Session.startSyncing(context: Context) { diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index e9e855e760..59eee14d37 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -32,12 +32,12 @@ import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.telecom.CallConnection import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.popup.IncomingCallAlert import im.vector.app.features.popup.PopupAlertManager import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber /** @@ -176,7 +176,7 @@ class CallService : VectorService() { } alertManager.postVectorAlert(incomingCallAlert) val notification = notificationUtils.buildIncomingCallNotification( - mxCall = call.mxCall, + call = call, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, fromBg = fromBg ) @@ -207,7 +207,7 @@ class CallService : VectorService() { private fun showCallScreen(call: WebRtcCall, mode: String) { val intent = VectorCallActivity.newIntent( context = this, - mxCall = call.mxCall, + call = call, mode = mode ) startActivity(intent) @@ -221,7 +221,7 @@ class CallService : VectorService() { val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayOutgoingCallNotification : display the dedicated notification") val notification = notificationUtils.buildOutgoingRingingCallNotification( - mxCall = call.mxCall, + call = call, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) if (knownCalls.isEmpty()) { @@ -244,7 +244,7 @@ class CallService : VectorService() { val opponentMatrixItem = getOpponentMatrixItem(call) alertManager.cancelAlert(callId) val notification = notificationUtils.buildPendingCallNotification( - mxCall = call.mxCall, + call = call, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) if (knownCalls.isEmpty()) { @@ -275,7 +275,9 @@ class CallService : VectorService() { } private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? { - return vectorComponent().currentSession().getUser(call.mxCall.opponentUserId)?.toMatrixItem() + return vectorComponent().activeSessionHolder().getSafeActiveSession()?.let { + call.getOpponentAsMatrixItem(it) + } } companion object { diff --git a/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt new file mode 100644 index 0000000000..ec99c7c215 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.ui.list + +import android.view.View +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +/** + * A generic item with empty space. + */ +@EpoxyModelClass(layout = R.layout.item_vertical_margin) +abstract class VerticalMarginItem : VectorEpoxyModel() { + + @EpoxyAttribute + var heightInPx: Int = 0 + + override fun bind(holder: Holder) { + super.bind(holder) + holder.space.updateLayoutParams { + height = heightInPx + } + } + + class Holder : VectorEpoxyHolder() { + val space by bind(R.id.item_vertical_margin_space) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/CallSessionDependencies.kt b/vector/src/main/java/im/vector/app/features/call/CallSessionDependencies.kt new file mode 100644 index 0000000000..d1b3f77604 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/CallSessionDependencies.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call + +import im.vector.app.features.call.lookup.CallProtocolsChecker +import im.vector.app.features.call.lookup.CallUserMapper +import im.vector.app.features.session.SessionScopedProperty +import org.matrix.android.sdk.api.session.Session + +interface VectorCallService { + val protocolChecker: CallProtocolsChecker + val userMapper: CallUserMapper +} + +val Session.vectorCallService: VectorCallService by SessionScopedProperty { + object : VectorCallService { + override val protocolChecker = CallProtocolsChecker(it) + override val userMapper = CallUserMapper(it, protocolChecker) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index a9e2982714..ad04e33414 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -46,6 +46,7 @@ import im.vector.app.databinding.ActivityCallBinding import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.utils.EglUtils +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity @@ -54,7 +55,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.MxCallDetail import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.webrtc.EglBase @@ -64,7 +64,7 @@ import javax.inject.Inject @Parcelize data class CallArgs( - val roomId: String, + val signalingRoomId: String, val callId: String, val participantUserId: String, val isIncomingCall: Boolean, @@ -175,7 +175,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro when (callState) { is CallState.Idle, is CallState.CreateOffer, - is CallState.Dialing -> { + is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_ring) @@ -189,16 +189,27 @@ class VectorCallActivity : VectorBaseActivity(), CallContro configureCallInfo(state) } - is CallState.Answering -> { + is CallState.Answering -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if (state.isLocalOnHold || state.isRemoteOnHold) { + if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { + val transfereeName = if (state.transferee is VectorCallViewState.TransfereeState.KnownTransferee) { + state.transferee.name + } else { + getString(R.string.call_transfer_unknown_person) + } + views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) + views.callActionText.isVisible = true + views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } + views.callStatusText.text = state.formattedDuration + configureCallInfo(state) + } else if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -220,7 +231,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false - views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null + views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null } else { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -235,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -247,7 +258,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro state.callInfo.otherUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) - views.participantNameText.text = it.getBestName() + if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { + views.participantNameText.text = it.getBestName() + } else { + views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) + } if (blurAvatar) { avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) } else { @@ -276,7 +291,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.otherKnownCallAvatarView.setOnClickListener { withState(callViewModel) { val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState - startActivity(newIntent(this, otherCall.mxCall, null)) + startActivity(newIntent(this, otherCall, null)) finish() } } @@ -322,13 +337,13 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.v("## VOIP handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { + VectorCallViewEvents.DismissNoCall -> { finish() } - is VectorCallViewEvents.ConnectionTimeout -> { + is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } - is VectorCallViewEvents.ShowDialPad -> { + is VectorCallViewEvents.ShowDialPad -> { CallDialPadBottomSheet.newInstance(false).apply { callback = dialPadCallback }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) @@ -336,7 +351,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro is VectorCallViewEvents.ShowCallTransferScreen -> { navigator.openCallTransfer(this, callArgs.callId) } - null -> { + null -> { } } } @@ -364,18 +379,18 @@ class VectorCallActivity : VectorBaseActivity(), CallContro const val INCOMING_RINGING = "INCOMING_RINGING" const val INCOMING_ACCEPT = "INCOMING_ACCEPT" - fun newIntent(context: Context, mxCall: MxCallDetail, mode: String?): Intent { + fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent { return Intent(context, VectorCallActivity::class.java).apply { // what could be the best flags? flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) + putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall)) putExtra(EXTRA_MODE, mode) } } fun newIntent(context: Context, callId: String, - roomId: String, + signalingRoomId: String, otherUserId: String, isIncomingCall: Boolean, isVideoCall: Boolean, @@ -383,7 +398,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro return Intent(context, VectorCallActivity::class.java).apply { // what could be the best flags? flags = FLAG_ACTIVITY_CLEAR_TOP - putExtra(MvRx.KEY_ARG, CallArgs(roomId, callId, otherUserId, isIncomingCall, isVideoCall)) + putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall)) putExtra(EXTRA_MODE, mode) } } @@ -410,7 +425,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } override fun returnToChat() { - val args = RoomDetailArgs(callArgs.roomId) + val args = RoomDetailArgs(callArgs.signalingRoomId) val intent = RoomDetailActivity.newIntent(this, args).apply { flags = FLAG_ACTIVITY_CLEAR_TOP } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index 7addabf724..a332153aaa 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions() + object TransferCall: VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 8a2d56a5a2..18eda0fd6f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -23,13 +23,14 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -38,8 +39,6 @@ import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer -import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.api.util.toMatrixItem class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, @@ -112,12 +111,21 @@ class VectorCallViewModel @AssistedInject constructor( setState { copy( callState = Success(callState), - canOpponentBeTransferred = call.capabilities.supportCallTransfer() + canOpponentBeTransferred = call.capabilities.supportCallTransfer(), + transferee = computeTransfereeState(call) ) } } } + private fun computeTransfereeState(call: MxCall): VectorCallViewState.TransfereeState { + val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return VectorCallViewState.TransfereeState.NoTransferee + val transfereeRoom = session.getRoomSummary(transfereeCall.nativeRoomId) + return transfereeRoom?.displayName?.let { + VectorCallViewState.TransfereeState.KnownTransferee(it) + } ?: VectorCallViewState.TransfereeState.UnknownTransferee + } + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { @@ -152,7 +160,7 @@ class VectorCallViewModel @AssistedInject constructor( if (otherCall == null) { copy(otherKnownCallInfo = null) } else { - val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem() + val otherUserItem = otherCall.getOpponentAsMatrixItem(session) copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem)) } } @@ -167,7 +175,7 @@ class VectorCallViewModel @AssistedInject constructor( } else { call = webRtcCall callManager.addCurrentCallListener(currentCallListener) - val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem() + val item = webRtcCall.getOpponentAsMatrixItem(session) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice if (currentSoundDevice == CallAudioManager.Device.PHONE) { @@ -186,7 +194,8 @@ class VectorCallViewModel @AssistedInject constructor( canSwitchCamera = webRtcCall.canSwitchCamera(), formattedDuration = webRtcCall.formattedDuration(), isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, - canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer() + canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(), + transferee = computeTransfereeState(webRtcCall.mxCall) ) } updateOtherKnownCall(webRtcCall) @@ -202,27 +211,27 @@ class VectorCallViewModel @AssistedInject constructor( override fun handle(action: VectorCallViewActions) = withState { state -> when (action) { - VectorCallViewActions.EndCall -> call?.endCall() - VectorCallViewActions.AcceptCall -> { + VectorCallViewActions.EndCall -> call?.endCall() + VectorCallViewActions.AcceptCall -> { setState { copy(callState = Loading()) } call?.acceptIncomingCall() } - VectorCallViewActions.DeclineCall -> { + VectorCallViewActions.DeclineCall -> { setState { copy(callState = Loading()) } call?.endCall() } - VectorCallViewActions.ToggleMute -> { + VectorCallViewActions.ToggleMute -> { val muted = state.isAudioMuted call?.muteCall(!muted) setState { copy(isAudioMuted = !muted) } } - VectorCallViewActions.ToggleVideo -> { + VectorCallViewActions.ToggleVideo -> { if (state.isVideoCall) { val videoEnabled = state.isVideoEnabled call?.enableVideo(!videoEnabled) @@ -232,14 +241,14 @@ class VectorCallViewModel @AssistedInject constructor( } Unit } - VectorCallViewActions.ToggleHoldResume -> { + VectorCallViewActions.ToggleHoldResume -> { val isRemoteOnHold = state.isRemoteOnHold call?.updateRemoteOnHold(!isRemoteOnHold) } is VectorCallViewActions.ChangeAudioDevice -> { callManager.audioManager.setAudioDevice(action.device) } - VectorCallViewActions.SwitchSoundDevice -> { + VectorCallViewActions.SwitchSoundDevice -> { _viewEvents.post( VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device) ) @@ -255,17 +264,17 @@ class VectorCallViewModel @AssistedInject constructor( } Unit } - VectorCallViewActions.ToggleCamera -> { + VectorCallViewActions.ToggleCamera -> { call?.switchCamera() } - VectorCallViewActions.ToggleHDSD -> { + VectorCallViewActions.ToggleHDSD -> { if (!state.isVideoCall) return@withState call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) } - VectorCallViewActions.OpenDialPad -> { + VectorCallViewActions.OpenDialPad -> { _viewEvents.post(VectorCallViewEvents.ShowDialPad) } - is VectorCallViewActions.SendDtmfDigit -> { + is VectorCallViewActions.SendDtmfDigit -> { call?.sendDtmfDigit(action.digit) } VectorCallViewActions.InitiateCallTransfer -> { @@ -273,9 +282,20 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } + VectorCallViewActions.TransferCall -> { + handleCallTransfer() + } }.exhaustive } + private fun handleCallTransfer() { + viewModelScope.launch { + val currentCall = call ?: return@launch + val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch + currentCall.transferToCall(transfereeCall) + } + } + @AssistedFactory interface Factory { fun create(initialState: VectorCallViewState): VectorCallViewModel diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index cdd002114a..c5ae61cf60 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -41,17 +41,24 @@ data class VectorCallViewState( val otherKnownCallInfo: CallInfo? = null, val callInfo: CallInfo = CallInfo(callId), val formattedDuration: String = "", - val canOpponentBeTransferred: Boolean = false + val canOpponentBeTransferred: Boolean = false, + val transferee: TransfereeState = TransfereeState.NoTransferee ) : MvRxState { + sealed class TransfereeState { + object NoTransferee : TransfereeState() + data class KnownTransferee(val name: String) : TransfereeState() + object UnknownTransferee : TransfereeState() + } + data class CallInfo( val callId: String, val otherUserItem: MatrixItem? = null ) - constructor(callArgs: CallArgs): this( + constructor(callArgs: CallArgs) : this( callId = callArgs.callId, - roomId = callArgs.roomId, + roomId = callArgs.signalingRoomId, isVideoCall = callArgs.isVideoCall ) } diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 6fccea6c8c..4ed1e4a0db 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -16,30 +16,24 @@ package im.vector.app.features.call.dialpad +import im.vector.app.features.call.lookup.pstnLookup import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper -import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import java.lang.IllegalStateException import javax.inject.Inject class DialPadLookup @Inject constructor( private val session: Session, - private val directRoomHelper: DirectRoomHelper, - private val callManager: WebRtcCallManager + private val webRtcCallManager: WebRtcCallManager, + private val directRoomHelper: DirectRoomHelper ) { class Failure : Throwable() data class Result(val userId: String, val roomId: String) suspend fun lookupPhoneNumber(phoneNumber: String): Result { - val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure() - val thirdPartyUser = tryOrNull { - session.thirdPartyService().getThirdPartyUser( - protocol = supportedProtocolKey, - fields = mapOf("m.id.phone" to phoneNumber) - ).firstOrNull() - } ?: throw Failure() - + val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw IllegalStateException() val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) return Result(userId = thirdPartyUser.userId, roomId = roomId) } diff --git a/vector/src/main/java/im/vector/app/features/call/lookup/CallProtocolsChecker.kt b/vector/src/main/java/im/vector/app/features/call/lookup/CallProtocolsChecker.kt new file mode 100644 index 0000000000..9f6a24fd25 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/lookup/CallProtocolsChecker.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.lookup + +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean + +const val PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn" +const val PROTOCOL_PSTN = "m.protocol.pstn" +const val PROTOCOL_SIP_NATIVE = "im.vector.protocol.sip_native" +const val PROTOCOL_SIP_VIRTUAL = "im.vector.protocol.sip_virtual" + +class CallProtocolsChecker(private val session: Session) { + + interface Listener { + fun onPSTNSupportUpdated() = Unit + fun onVirtualRoomSupportUpdated() = Unit + } + + private val alreadyChecked = AtomicBoolean(false) + private val checking = AtomicBoolean(false) + + private val listeners = mutableListOf() + + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + var supportedPSTNProtocol: String? = null + private set + + var supportVirtualRooms: Boolean = false + private set + + fun checkProtocols() { + session.coroutineScope.launch { + checkThirdPartyProtocols() + } + } + + suspend fun awaitCheckProtocols() { + checkThirdPartyProtocols() + } + + private suspend fun checkThirdPartyProtocols() { + if (alreadyChecked.get()) return + if (!checking.compareAndSet(false, true)) return + try { + val protocols = getThirdPartyProtocols(3) + alreadyChecked.set(true) + checking.set(false) + supportedPSTNProtocol = protocols.extractPSTN() + if (supportedPSTNProtocol != null) { + listeners.forEach { + tryOrNull { it.onPSTNSupportUpdated() } + } + } + supportVirtualRooms = protocols.supportsVirtualRooms() + if (supportVirtualRooms) { + listeners.forEach { + tryOrNull { it.onVirtualRoomSupportUpdated() } + } + } + } catch (failure: Throwable) { + Timber.v("Fail to get third party protocols, will check again next time.") + } + } + + private fun Map.extractPSTN(): String? { + return when { + containsKey(PROTOCOL_PSTN_PREFIXED) -> PROTOCOL_PSTN_PREFIXED + containsKey(PROTOCOL_PSTN) -> PROTOCOL_PSTN + else -> null + } + } + + private fun Map.supportsVirtualRooms(): Boolean { + return containsKey(PROTOCOL_SIP_VIRTUAL) && containsKey(PROTOCOL_SIP_NATIVE) + } + + private suspend fun getThirdPartyProtocols(maxTries: Int): Map { + return try { + session.thirdPartyService().getThirdPartyProtocols() + } catch (failure: Throwable) { + if (maxTries == 1) { + throw failure + } else { + // Wait for 10s before trying again + delay(10_000L) + return getThirdPartyProtocols(maxTries - 1) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/lookup/CallUserMapper.kt b/vector/src/main/java/im/vector/app/features/call/lookup/CallUserMapper.kt new file mode 100644 index 0000000000..04177bd2b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/lookup/CallUserMapper.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.lookup + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams + +class CallUserMapper(private val session: Session, private val protocolsChecker: CallProtocolsChecker) { + + fun nativeRoomForVirtualRoom(roomId: String): String? { + val virtualRoom = session.getRoom(roomId) ?: return null + val virtualRoomEvent = virtualRoom.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM) + return virtualRoomEvent?.content?.toModel()?.nativeRoomId + } + + suspend fun getOrCreateVirtualRoomForRoom(roomId: String, opponentUserId: String): String? { + protocolsChecker.awaitCheckProtocols() + if (!protocolsChecker.supportVirtualRooms) return null + val virtualUser = userToVirtualUser(opponentUserId) ?: return null + val virtualRoomId = tryOrNull { + ensureVirtualRoomExists(virtualUser, roomId) + } ?: return null + session.getRoom(virtualRoomId)?.markVirtual(roomId) + return virtualRoomId + } + + suspend fun onNewInvitedRoom(invitedRoomId: String) { + protocolsChecker.awaitCheckProtocols() + if (!protocolsChecker.supportVirtualRooms) return + val invitedRoom = session.getRoom(invitedRoomId) ?: return + val inviterId = invitedRoom.roomSummary()?.inviterId ?: return + val nativeLookup = session.sipNativeLookup(inviterId).firstOrNull() ?: return + if (nativeLookup.fields.containsKey("is_virtual")) { + val nativeUser = nativeLookup.userId + val nativeRoomId = session.getExistingDirectRoomWithUser(nativeUser) + if (nativeRoomId != null) { + // It's a virtual room with a matching native room, so set the room account data. This + // will make sure we know where how to map calls and also allow us know not to display + // it in the future. + invitedRoom.markVirtual(nativeRoomId) + // also auto-join the virtual room if we have a matching native room + // (possibly we should only join if we've also joined the native room, then we'd also have + // to make sure we joined virtual rooms on joining a native one) + session.joinRoom(invitedRoomId) + } + } + } + + private suspend fun userToVirtualUser(userId: String): String? { + val results = session.sipVirtualLookup(userId) + return results.firstOrNull()?.userId + } + + private suspend fun Room.markVirtual(nativeRoomId: String) { + val virtualRoomContent = RoomVirtualContent(nativeRoomId = nativeRoomId) + updateAccountData(RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM, virtualRoomContent.toContent()) + } + + private suspend fun ensureVirtualRoomExists(userId: String, nativeRoomId: String): String { + val existingDMRoom = tryOrNull { session.getExistingDirectRoomWithUser(userId) } + val roomId: String + if (existingDMRoom != null) { + roomId = existingDMRoom + } else { + val roomParams = CreateRoomParams().apply { + invitedUserIds.add(userId) + setDirectMessage() + creationContent[RoomAccountDataTypes.EVENT_TYPE_VIRTUAL_ROOM] = nativeRoomId + } + roomId = session.createRoom(roomParams) + } + return roomId + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/lookup/RoomVirtualContent.kt b/vector/src/main/java/im/vector/app/features/call/lookup/RoomVirtualContent.kt new file mode 100644 index 0000000000..4f76f940e3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/lookup/RoomVirtualContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.lookup + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RoomVirtualContent( + @Json(name = "native_room") val nativeRoomId: String +) diff --git a/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt b/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt new file mode 100644 index 0000000000..1e9834059f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/lookup/ThirdPartyLookup.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.lookup + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser + +suspend fun Session.pstnLookup(phoneNumber: String, protocol: String?): List { + if (protocol == null) return emptyList() + return tryOrNull { + thirdPartyService().getThirdPartyUser( + protocol = protocol, + fields = mapOf("m.id.phone" to phoneNumber) + ) + }.orEmpty() +} + +suspend fun Session.sipVirtualLookup(nativeMxid: String): List { + return tryOrNull { + thirdPartyService().getThirdPartyUser( + protocol = PROTOCOL_SIP_VIRTUAL, + fields = mapOf("native_mxid" to nativeMxid) + ) + }.orEmpty() +} + +suspend fun Session.sipNativeLookup(virtualMxid: String): List { + return tryOrNull { + thirdPartyService().getThirdPartyUser( + protocol = PROTOCOL_SIP_NATIVE, + fields = mapOf("virtual_mxid" to virtualMxid) + ) + }.orEmpty() +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index 5f661faf80..0f37ccaa29 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -28,13 +28,16 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.createdirect.DirectRoomHelper import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, private val dialPadLookup: DialPadLookup, - callManager: WebRtcCallManager) + private val directRoomHelper: DirectRoomHelper, + private val callManager: WebRtcCallManager) : VectorViewModel(initialState) { @AssistedFactory @@ -75,7 +78,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: CallTransferAction) { when (action) { - is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) + is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action) }.exhaustive } @@ -83,8 +86,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) { viewModelScope.launch { try { - _viewEvents.post(CallTransferViewEvents.Loading) - call?.mxCall?.transfer(action.selectedUserId, null) + if (action.consultFirst) { + val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId) + callManager.startOutgoingCall( + nativeRoomId = dmRoomId, + otherUserId = action.selectedUserId, + isVideoCall = call?.mxCall?.isVideoCall.orFalse(), + transferee = call + ) + } else { + call?.transferToUser(action.selectedUserId, null) + } _viewEvents.post(CallTransferViewEvents.Dismiss) } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) @@ -97,7 +109,16 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: try { _viewEvents.post(CallTransferViewEvents.Loading) val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) - call?.mxCall?.transfer(result.userId, result.roomId) + if (action.consultFirst) { + callManager.startOutgoingCall( + nativeRoomId = result.roomId, + otherUserId = result.userId, + isVideoCall = call?.mxCall?.isVideoCall.orFalse(), + transferee = call + ) + } else { + call?.transferToUser(result.userId, result.roomId) + } _viewEvents.post(CallTransferViewEvents.Dismiss) } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index a3a1a29c4b..f2a008feb7 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.call.CallIdGenerator import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxPeerConnectionState @@ -85,14 +86,19 @@ private const val AUDIO_TRACK_ID = "ARDAMSa0" private const val VIDEO_TRACK_ID = "ARDAMSv0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() -class WebRtcCall(val mxCall: MxCall, - private val rootEglBase: EglBase?, - private val context: Context, - private val dispatcher: CoroutineContext, - private val sessionProvider: Provider, - private val peerConnectionFactoryProvider: Provider, - private val onCallBecomeActive: (WebRtcCall) -> Unit, - private val onCallEnded: (String) -> Unit) : MxCall.StateListener { +class WebRtcCall( + val mxCall: MxCall, + // This is where the call is placed from an ui perspective. + // In case of virtual room, it can differs from the signalingRoomId. + val nativeRoomId: String, + private val rootEglBase: EglBase?, + private val context: Context, + private val dispatcher: CoroutineContext, + private val sessionProvider: Provider, + private val peerConnectionFactoryProvider: Provider, + private val onCallBecomeActive: (WebRtcCall) -> Unit, + private val onCallEnded: (String) -> Unit +) : MxCall.StateListener { interface Listener : MxCall.StateListener { fun onCaptureStateChanged() {} @@ -116,7 +122,9 @@ class WebRtcCall(val mxCall: MxCall, } val callId = mxCall.callId - val roomId = mxCall.roomId + + // room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId. + val signalingRoomId = mxCall.roomId private var peerConnection: PeerConnection? = null private var localAudioSource: AudioSource? = null @@ -268,7 +276,7 @@ class WebRtcCall(val mxCall: MxCall, sessionScope?.launch(dispatcher) { when (mode) { - VectorCallActivity.INCOMING_ACCEPT -> { + VectorCallActivity.INCOMING_ACCEPT -> { internalAcceptIncomingCall() } VectorCallActivity.INCOMING_RINGING -> { @@ -286,6 +294,40 @@ class WebRtcCall(val mxCall: MxCall, } } + /** + * Without consultation + */ + suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { + mxCall.transfer( + targetUserId = targetUserId, + targetRoomId = targetRoomId, + createCallId = CallIdGenerator.generate(), + awaitCallId = null + ) + endCall(sendEndSignaling = false) + } + + /** + * With consultation + */ + suspend fun transferToCall(transferTargetCall: WebRtcCall) { + val newCallId = CallIdGenerator.generate() + transferTargetCall.mxCall.transfer( + targetUserId = mxCall.opponentUserId, + targetRoomId = null, + createCallId = null, + awaitCallId = newCallId + ) + mxCall.transfer( + targetUserId = transferTargetCall.mxCall.opponentUserId, + targetRoomId = null, + createCallId = newCallId, + awaitCallId = null + ) + endCall(sendEndSignaling = false) + transferTargetCall.endCall(sendEndSignaling = false) + } + fun acceptIncomingCall() { sessionScope?.launch { Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") @@ -385,6 +427,7 @@ class WebRtcCall(val mxCall: MxCall, peerConnection?.awaitSetRemoteDescription(offerSdp) } catch (failure: Throwable) { Timber.v("Failure putting remote description") + endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR) return@withContext } // 2) Access camera + microphone, create local stream @@ -725,7 +768,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) { if (mxCall.state == CallState.Terminated) { return } @@ -740,9 +783,9 @@ class WebRtcCall(val mxCall: MxCall, mxCall.state = CallState.Terminated sessionScope?.launch(dispatcher) { release() + onCallEnded(callId) } - onCallEnded(callId) - if (originatedByMe) { + if (sendEndSignaling) { if (wasRinging) { mxCall.reject() } else { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt new file mode 100644 index 0000000000..c99d097707 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.webrtc + +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem + +fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? { + return session.getRoomSummary(nativeRoomId)?.otherMemberIds?.firstOrNull()?.let { + session.getUser(it)?.toMatrixItem() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 2f8f84051e..3c18d97937 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -24,15 +24,18 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.core.services.CallService import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.call.lookup.CallProtocolsChecker +import im.vector.app.features.call.lookup.CallUserMapper import im.vector.app.features.call.utils.EglUtils +import im.vector.app.features.call.vectorCallService import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.asCoroutineDispatcher +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall -import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent @@ -64,8 +67,11 @@ class WebRtcCallManager @Inject constructor( private val currentSession: Session? get() = activeSessionDataSource.currentValue?.orNull() - private val pstnProtocolChecker: PSTNProtocolChecker? - get() = currentSession?.callSignalingService()?.getPSTNProtocolChecker() + private val protocolsChecker: CallProtocolsChecker? + get() = currentSession?.vectorCallService?.protocolChecker + + private val callUserMapper: CallUserMapper? + get() = currentSession?.vectorCallService?.userMapper interface CurrentCallListener { fun onCurrentCallChange(call: WebRtcCall?) {} @@ -73,17 +79,20 @@ class WebRtcCallManager @Inject constructor( } val supportedPSTNProtocol: String? - get() = pstnProtocolChecker?.supportedPSTNProtocol + get() = protocolsChecker?.supportedPSTNProtocol val supportsPSTNProtocol: Boolean get() = supportedPSTNProtocol != null - fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) { - pstnProtocolChecker?.addListener(listener) + val supportsVirtualRooms: Boolean + get() = protocolsChecker?.supportVirtualRooms.orFalse() + + fun addProtocolsCheckerListener(listener: CallProtocolsChecker.Listener) { + protocolsChecker?.addListener(listener) } - fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) { - pstnProtocolChecker?.removeListener(listener) + fun removeProtocolsCheckerListener(listener: CallProtocolsChecker.Listener) { + protocolsChecker?.removeListener(listener) } private val currentCallsListeners = CopyOnWriteArrayList() @@ -138,6 +147,11 @@ class WebRtcCallManager @Inject constructor( private val callsByCallId = ConcurrentHashMap() private val callsByRoomId = ConcurrentHashMap>() + // Calls started as an attended transfer, ie. with the intention of transferring another + // call with a different party to this one. + // callId (target) -> call (transferee) + private val transferees = ConcurrentHashMap() + fun getCallById(callId: String): WebRtcCall? { return callsByCallId[callId] } @@ -146,6 +160,10 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getTransfereeForCallId(callId: String): WebRtcCall? { + return transferees[callId] + } + fun getCurrentCall(): WebRtcCall? { return currentCall.get() } @@ -154,8 +172,8 @@ class WebRtcCallManager @Inject constructor( return callsByCallId.values.toList() } - fun checkForPSTNSupportIfNeeded() { - pstnProtocolChecker?.checkForPSTNSupportIfNeeded() + fun checkForProtocolsSupportIfNeeded() { + protocolsChecker?.checkProtocols() } /** @@ -218,36 +236,35 @@ class WebRtcCallManager @Inject constructor( Timber.v("On call ended for unknown call $callId") } CallService.onCallTerminated(context, callId) - callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall) + callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) + callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) + transferees.remove(callId) if (getCurrentCall()?.callId == callId) { val otherCall = getCalls().lastOrNull() currentCall.setAndNotify(otherCall) } - // This must be done in this thread - executor.execute { - // There is no active calls - if (getCurrentCall() == null) { - Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") - peerConnectionFactory?.dispose() - peerConnectionFactory = null - audioManager.setMode(CallAudioManager.Mode.DEFAULT) - // did we start background sync? so we should stop it - if (isInBackground) { - if (FcmHelper.isPushSupported()) { - currentSession?.stopAnyBackgroundSync() - } else { - // for fdroid we should not stop, it should continue syncing - // maybe we should restore default timeout/delay though? - } + // There is no active calls + if (getCurrentCall() == null) { + Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") + peerConnectionFactory?.dispose() + peerConnectionFactory = null + audioManager.setMode(CallAudioManager.Mode.DEFAULT) + // did we start background sync? so we should stop it + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + currentSession?.stopAnyBackgroundSync() + } else { + // for fdroid we should not stop, it should continue syncing + // maybe we should restore default timeout/delay though? } } - Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done") } } - fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { + suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) { + val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") - if (getCallsByRoomId(signalingRoomId).isNotEmpty()) { + if (getCallsByRoomId(nativeRoomId).isNotEmpty()) { Timber.w("## VOIP you already have a call in this room") return } @@ -261,15 +278,17 @@ class WebRtcCallManager @Inject constructor( } getCurrentCall()?.updateRemoteOnHold(onHold = true) val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - val webRtcCall = createWebRtcCall(mxCall) + val webRtcCall = createWebRtcCall(mxCall, nativeRoomId) currentCall.setAndNotify(webRtcCall) - + if (transferee != null) { + transferees[webRtcCall.callId] = transferee + } CallService.onOutgoingCallRinging( context = context.applicationContext, callId = mxCall.callId) // start the activity now - context.startActivity(VectorCallActivity.newIntent(context, mxCall, VectorCallActivity.OUTGOING_CREATED)) + context.startActivity(VectorCallActivity.newIntent(context, webRtcCall, VectorCallActivity.OUTGOING_CREATED)) } override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { @@ -281,9 +300,10 @@ class WebRtcCallManager @Inject constructor( call.onCallIceCandidateReceived(iceCandidatesContent) } - private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { + private fun createWebRtcCall(mxCall: MxCall, nativeRoomId: String): WebRtcCall { val webRtcCall = WebRtcCall( mxCall = mxCall, + nativeRoomId = nativeRoomId, rootEglBase = rootEglBase, context = context, dispatcher = dispatcher, @@ -297,6 +317,8 @@ class WebRtcCallManager @Inject constructor( ) advertisedCalls.add(mxCall.callId) callsByCallId[mxCall.callId] = webRtcCall + callsByRoomId.getOrPut(nativeRoomId) { ArrayList(1) } + .add(webRtcCall) callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) } .add(webRtcCall) if (getCurrentCall() == null) { @@ -306,12 +328,13 @@ class WebRtcCallManager @Inject constructor( } fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) { - callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) } + callsByRoomId[roomId]?.firstOrNull()?.endCall(originatedByMe) } override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (getCallsByRoomId(mxCall.roomId).isNotEmpty()) { + val nativeRoomId = callUserMapper?.nativeRoomForVirtualRoom(mxCall.roomId) ?: mxCall.roomId + if (getCallsByRoomId(nativeRoomId).isNotEmpty()) { Timber.w("## VOIP you already have a call in this room") return } @@ -320,7 +343,7 @@ class WebRtcCallManager @Inject constructor( // Just ignore, maybe we could answer from other session? return } - createWebRtcCall(mxCall).apply { + createWebRtcCall(mxCall, nativeRoomId).apply { offerSdp = callInviteContent.offer } // Start background service with notification diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 0b210cf298..61d39857cc 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -46,7 +46,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d PLAIN("/plain", "", R.string.command_description_plain, false), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), CONFETTI("/confetti", "", R.string.command_confetti, false), - SNOW("/snow", "", R.string.command_snow, false), + SNOWFALL("/snowfall", "", R.string.command_snow, false), CREATE_SPACE("/createspace", " *", R.string.command_description_create_space, true), ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 9b190d64fe..3de00f4d0c 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -296,9 +296,9 @@ object CommandParser { val message = textMessage.substring(Command.CONFETTI.command.length).trim() ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) } - Command.SNOW.command -> { - val message = textMessage.substring(Command.SNOW.command.length).trim() - ParsedCommand.SendChatEffect(ChatEffect.SNOW, message) + Command.SNOWFALL.command -> { + val message = textMessage.substring(Command.SNOWFALL.command.length).trim() + ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message) } Command.CREATE_SPACE.command -> { val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim() diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt index d2cf871701..f8290c0321 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -252,7 +252,7 @@ class KeysBackupRestoreSharedViewModel @Inject constructor( } private fun isBackupKeyInQuadS(): Boolean { - val sssBackupSecret = session.getAccountDataEvent(KEYBACKUP_SECRET_SSSS_NAME) + val sssBackupSecret = session.userAccountDataService().getAccountDataEvent(KEYBACKUP_SECRET_SSSS_NAME) ?: return false // Some sanity ? diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 11a30b304e..f55b482124 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -218,7 +218,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( withContext(Dispatchers.IO) { args.requestedSecrets.forEach { - if (session.getAccountDataEvent(it) != null) { + if (session.userAccountDataService().getAccountDataEvent(it) != null) { val res = session.sharedSecretStorageService.getSecret( name = it, keyId = keyInfo.id, @@ -287,7 +287,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( withContext(Dispatchers.IO) { args.requestedSecrets.forEach { - if (session.getAccountDataEvent(it) != null) { + if (session.userAccountDataService().getAccountDataEvent(it) != null) { val res = session.sharedSecretStorageService.getSecret( name = it, keyId = keyInfo.id, diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index b59b24fe55..47059128a1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -33,6 +33,9 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder() { var inputType: Int? = null @EpoxyAttribute - var singleLine: Boolean? = null + var singleLine: Boolean = true @EpoxyAttribute var imeOptions: Int? = null @@ -60,9 +61,13 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var endIconMode: Int? = null - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + // FIXME restore EpoxyAttribute.Option.DoNotHash and fix that properly + @EpoxyAttribute var onTextChange: ((String) -> Unit)? = null + @EpoxyAttribute + var editorActionListener: TextView.OnEditorActionListener? = null + private val onTextChangeListener = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { onTextChange?.invoke(s.toString()) @@ -80,10 +85,11 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } - holder.textInputEditText.isSingleLine = singleLine ?: false + holder.textInputEditText.isSingleLine = singleLine holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE holder.textInputEditText.addTextChangedListener(onTextChangeListener) + holder.textInputEditText.setOnEditorActionListener(editorActionListener) holder.bottomSeparator.isVisible = showBottomSeparator } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt index 0a07d27f64..b02e5c52df 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt @@ -71,7 +71,7 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder { - avatarRenderer?.renderSpace(matrixItem!!, holder.image) + avatarRenderer?.render(matrixItem!!, holder.image) } else -> { avatarRenderer?.clear(holder.image) diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 65bc5e1200..c6cceee3b9 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -66,24 +66,24 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active DrawableImageViewTarget(imageView)) } - @UiThread - fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { - val placeholder = getSpacePlaceholderDrawable(matrixItem) - val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) - glideRequests - .load(resolvedUrl) - .transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) - .placeholder(placeholder) - .into(DrawableImageViewTarget(imageView)) - } - - fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) { - renderSpace( - matrixItem, - imageView, - GlideApp.with(imageView) - ) - } +// fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) { +// renderSpace( +// matrixItem, +// imageView, +// GlideApp.with(imageView) +// ) +// } +// +// @UiThread +// private fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { +// val placeholder = getSpacePlaceholderDrawable(matrixItem) +// val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) +// glideRequests +// .load(resolvedUrl) +// .transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) +// .placeholder(placeholder) +// .into(DrawableImageViewTarget(imageView)) +// } fun clear(imageView: ImageView) { // It can be called after recycler view is destroyed, just silently catch @@ -137,7 +137,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active target: Target) { val placeholder = getPlaceholderDrawable(matrixItem) buildGlideRequest(glideRequests, matrixItem.avatarUrl) - .apply(RequestOptions.circleCropTransform()) + .apply { + when (matrixItem) { + is MatrixItem.SpaceItem -> { + transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) + } + else -> { + apply(RequestOptions.circleCropTransform()) + } + } + } .placeholder(placeholder) .into(target) } @@ -197,17 +206,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .beginConfig() .bold() .endConfig() - .buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor) - } - - @AnyThread - fun getSpacePlaceholderDrawable(matrixItem: MatrixItem): Drawable { - val avatarColor = matrixItemColorProvider.getColor(matrixItem) - return TextDrawable.builder() - .beginConfig() - .bold() - .endConfig() - .buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8)) + .let { + when (matrixItem) { + is MatrixItem.SpaceItem -> { + it.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8)) + } + else -> { + it.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor) + } + } + } } // PRIVATE API ********************************************************************************* diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 4d28823dd2..1c76eabeba 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -456,7 +456,7 @@ class HomeDetailFragment @Inject constructor( VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, - roomId = call.mxCall.roomId, + signalingRoomId = call.signalingRoomId, otherUserId = call.mxCall.opponentUserId, isIncomingCall = !call.mxCall.isOutgoing, isVideoCall = call.mxCall.isVideoCall, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index e7136762d5..24151c5c10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -26,13 +26,13 @@ import javax.inject.Inject enum class ChatEffect { CONFETTI, - SNOW + SNOWFALL } fun ChatEffect.toMessageType(): String { return when (this) { ChatEffect.CONFETTI -> MessageType.MSGTYPE_CONFETTI - ChatEffect.SNOW -> MessageType.MSGTYPE_SNOW + ChatEffect.SNOWFALL -> MessageType.MSGTYPE_SNOWFALL } } @@ -112,14 +112,14 @@ class ChatEffectManager @Inject constructor() { private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? { return when (content.msgType) { MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI - MessageType.MSGTYPE_SNOW -> ChatEffect.SNOW + MessageType.MSGTYPE_SNOWFALL -> ChatEffect.SNOWFALL MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_TEXT -> { event.root.getClearContent().toModel()?.body ?.let { text -> when { EMOJIS_FOR_CONFETTI.any { text.contains(it) } -> ChatEffect.CONFETTI - EMOJIS_FOR_SNOW.any { text.contains(it) } -> ChatEffect.SNOW + EMOJIS_FOR_SNOWFALL.any { text.contains(it) } -> ChatEffect.SNOWFALL else -> null } } @@ -133,7 +133,7 @@ class ChatEffectManager @Inject constructor() { "🎉", "🎊" ) - private val EMOJIS_FOR_SNOW = listOf( + private val EMOJIS_FOR_SNOWFALL = listOf( "⛄️", "☃️", "❄️" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index b6d3fbe893..7e26d79474 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -419,7 +419,7 @@ class RoomDetailFragment @Inject constructor( private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { val intent = VectorCallActivity.newIntent( context = vectorBaseActivity, - mxCall = event.call.mxCall, + call = event.call, mode = VectorCallActivity.INCOMING_ACCEPT ) startActivity(intent) @@ -440,7 +440,7 @@ class RoomDetailFragment @Inject constructor( .setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f) .streamFor(150, 3000L) } - ChatEffect.SNOW -> { + ChatEffect.SNOWFALL -> { views.viewSnowFall.isVisible = true views.viewSnowFall.restartFalling() } @@ -2054,7 +2054,7 @@ class RoomDetailFragment @Inject constructor( VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, - roomId = call.roomId, + signalingRoomId = call.signalingRoomId, otherUserId = call.mxCall.opponentUserId, isIncomingCall = !call.mxCall.isOutgoing, isVideoCall = call.mxCall.isVideoCall, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 084cd6d13c..e6b618f150 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup +import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand @@ -69,7 +70,6 @@ import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho @@ -122,7 +122,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val jitsiService: JitsiService, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), - Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { + Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -186,8 +186,8 @@ class RoomDetailViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { tryOrNull { session.onRoomDisplayed(initialState.roomId) } } - callManager.addPstnSupportListener(this) - callManager.checkForPSTNSupportIfNeeded() + callManager.addProtocolsCheckerListener(this) + callManager.checkForProtocolsSupportIfNeeded() chatEffectManager.delegate = this // Ensure to share the outbound session keys with all members @@ -331,7 +331,7 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) { viewModelScope.launch { try { - val result = DialPadLookup(session, directRoomHelper, callManager).lookupPhoneNumber(action.phoneNumber) + val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(action.phoneNumber) callManager.startOutgoingCall(result.roomId, result.userId, action.videoCall) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) @@ -392,8 +392,10 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleStartCall(action: RoomDetailAction.StartCall) { - room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { - callManager.startOutgoingCall(room.roomId, it, action.isVideo) + viewModelScope.launch { + room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { + callManager.startOutgoingCall(room.roomId, it, action.isVideo) + } } } @@ -894,7 +896,7 @@ class RoomDetailViewModel @AssistedInject constructor( if (sendChatEffect.message.isBlank()) { val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { ChatEffect.CONFETTI -> R.string.default_message_emote_confetti - ChatEffect.SNOW -> R.string.default_message_emote_snow + ChatEffect.SNOWFALL -> R.string.default_message_emote_snow }) room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) } else { @@ -1515,7 +1517,7 @@ class RoomDetailViewModel @AssistedInject constructor( } chatEffectManager.delegate = null chatEffectManager.dispose() - callManager.removePstnSupportListener(this) + callManager.removeProtocolsCheckerListener(this) super.onCleared() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index 30f1ecdc6d..cf508a2dab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -103,7 +103,7 @@ class StartCallActionsHandler( val currentCall = callManager.getCurrentCall() if (currentCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? - if (currentCall.roomId == roomId) { + if (currentCall.signalingRoomId == roomId) { onTapToReturnToCall() } // else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 3df9898078..9dcc3e8182 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent -import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -51,7 +51,7 @@ class CallItemFactory @Inject constructor( if (event.root.eventId == null) return null val roomId = event.roomId val informationData = messageInformationDataFactory.create(params) - val callSignalingContent = event.getCallSignallingContent() ?: return null + val callSignalingContent = event.getCallSignalingContent() ?: return null val callId = callSignalingContent.callId ?: return null val call = callManager.getCallById(callId) val callKind = when { @@ -112,7 +112,7 @@ class CallItemFactory @Inject constructor( } } - private fun TimelineEvent.getCallSignallingContent(): CallSignallingContent? { + private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? { return when (root.getClearType()) { EventType.CALL_INVITE -> root.getClearContent().toModel() EventType.CALL_HANGUP -> root.getClearContent().toModel() diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index d583f2ae9f..1e27975538 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -32,4 +32,5 @@ sealed class RoomListAction : VectorViewModelAction { data class SetMarkedUnread(val roomId: String, val markedUnread: Boolean) : RoomListAction() data class SetSectionExpanded(val section: RoomsSection, val expanded: Boolean) : RoomListAction() data class JoinSuggestedRoom(val roomId: String, val viaServers: List?) : RoomListAction() + data class ShowRoomDetails(val roomId: String, val viaServers: List?) : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 123fdcc6cb..7128150271 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -117,10 +117,11 @@ class RoomListFragment @Inject constructor( sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) roomListViewModel.observeViewEvents { when (it) { - is RoomListViewEvents.Loading -> showLoading(it.message) - is RoomListViewEvents.Failure -> showFailure(it.throwable) - is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) - is RoomListViewEvents.Done -> Unit + is RoomListViewEvents.Loading -> showLoading(it.message) + is RoomListViewEvents.Failure -> showFailure(it.throwable) + is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) + is RoomListViewEvents.Done -> Unit + is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link) }.exhaustive } @@ -171,6 +172,10 @@ class RoomListFragment @Inject constructor( showErrorInSnackbar(throwable) } + private fun handleShowMxToLink(link: String) { + navigator.openMatrixToBottomSheet(requireContext(), link) + } + override fun onDestroyView() { adapterInfosList.onEach { it.contentEpoxyController.removeModelBuildListener(modelBuildListener) } adapterInfosList.clear() @@ -519,6 +524,10 @@ class RoomListFragment @Inject constructor( roomListViewModel.handle(RoomListAction.JoinSuggestedRoom(room.childRoomId, room.viaServers)) } + override fun onSuggestedRoomClicked(room: SpaceChildInfo) { + roomListViewModel.handle(RoomListAction.ShowRoomDetails(room.childRoomId, room.viaServers)) + } + override fun onRejectRoomInvitation(room: RoomSummary) { notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) roomListViewModel.handle(RoomListAction.RejectInvitation(room)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt index 0ba265f841..cf619ce435 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt @@ -26,4 +26,5 @@ interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListen fun onRejectRoomInvitation(room: RoomSummary) fun onAcceptRoomInvitation(room: RoomSummary) fun onJoinSuggestedRoom(room: SpaceChildInfo) + fun onSuggestedRoomClicked(room: SpaceChildInfo) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt index cb84d91373..df2ff58da6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt @@ -29,4 +29,5 @@ sealed class RoomListViewEvents : VectorViewEvents { data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents() object Done : RoomListViewEvents() + data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 0be06bb8f0..9eda13a168 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -167,6 +167,7 @@ class RoomListViewModel @Inject constructor( is RoomListAction.SetSectionExpanded -> handleSetSectionExpanded(action.section, action.expanded) is RoomListAction.ToggleSection -> handleToggleSection(action.section) is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action) + is RoomListAction.ShowRoomDetails -> handleShowRoomDetails(action) }.exhaustive } @@ -299,6 +300,12 @@ class RoomListViewModel @Inject constructor( } } + private fun handleShowRoomDetails(action: RoomListAction.ShowRoomDetails) { + session.permalinkService().createRoomPermalink(action.roomId, action.viaServers)?.let { + _viewEvents.post(RoomListViewEvents.NavigateToMxToBottomSheet(it)) + } + } + private fun handleToggleTag(action: RoomListAction.ToggleTag) { session.getRoom(action.roomId)?.let { room -> viewModelScope.launch(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 687036fe3e..c7026ad374 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.list -import android.view.View import com.airbnb.mvrx.Async import com.airbnb.mvrx.Loading import im.vector.app.R @@ -58,7 +57,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun createSuggestion(spaceChildInfo: SpaceChildInfo, suggestedRoomJoiningStates: Map>, - onJoinClick: View.OnClickListener): VectorEpoxyModel<*> { + listener: RoomListListener?): VectorEpoxyModel<*> { return SpaceChildInfoItem_() .id("sug_${spaceChildInfo.childRoomId}") .matrixItem(spaceChildInfo.toMatrixItem()) @@ -67,7 +66,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .buttonLabel(stringProvider.getString(R.string.join)) .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading) .memberCount(spaceChildInfo.activeMemberCount ?: 0) - .buttonClickListener(onJoinClick) + .buttonClickListener(DebouncedClickListener({ listener?.onJoinSuggestedRoom(spaceChildInfo) })) + .itemClickListener(DebouncedClickListener({ listener?.onSuggestedRoomClicked(spaceChildInfo) })) } private fun createInvitationItem(roomSummary: RoomSummary, diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt index cb9c8b1f2e..65b42f437b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt @@ -48,7 +48,6 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel( @EpoxyAttribute var memberCount: Int = 0 @EpoxyAttribute var loading: Boolean = false - @EpoxyAttribute var space: Boolean = false @EpoxyAttribute var buttonLabel: String? = null @@ -63,12 +62,8 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel( it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) itemLongClickListener?.onLongClick(it) ?: false } - holder.titleView.text = matrixItem.getBestName() - if (space) { - avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) - } else { - avatarRenderer.render(matrixItem, holder.avatarImageView) - } + holder.titleView.text = matrixItem.displayName ?: holder.rootView.context.getString(R.string.unnamed_room) + avatarRenderer.render(matrixItem, holder.avatarImageView) holder.descriptionText.text = span { span { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt index 073fb43b6c..f9775967ee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt @@ -24,11 +24,7 @@ class SuggestedRoomListController( override fun buildModels(data: SuggestedRoomInfo?) { data?.rooms?.forEach { info -> - roomSummaryItemFactory.createSuggestion(info, data.joinEcho) { - listener?.onJoinSuggestedRoom(info) - }.let { - add(it) - } + add(roomSummaryItemFactory.createSuggestion(info, data.joinEcho, listener)) } } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt index 2f341d48ec..40213dc0ee 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -41,14 +41,15 @@ data class MatrixToBottomSheetState( sealed class RoomInfoResult { data class FullInfo( - val roomItem: MatrixItem.RoomItem, + val roomItem: MatrixItem, val name: String, val topic: String, val memberCount: Int?, val alias: String?, val membership: Membership, val roomType: String?, - val viaServers: List? + val viaServers: List?, + val isPublic: Boolean ) : RoomInfoResult() data class PartialInfo( diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 694f324025..1c78348b56 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -118,11 +118,9 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( session.getRoom(permalinkData.roomIdOrAlias) } ?.roomSummary() - // don't take if not active, as it could be outdated - ?.takeIf { it.membership.isActive() } - // XXX fix that - val forceRefresh = true - if (!forceRefresh && knownRoom != null) { + // don't take if not Join, as it could be outdated + ?.takeIf { it.membership == Membership.JOIN } + if (knownRoom != null) { setState { copy( roomPeekResult = Success( @@ -134,7 +132,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( alias = knownRoom.canonicalAlias, membership = knownRoom.membership, roomType = knownRoom.roomType, - viaServers = null + viaServers = null, + isPublic = knownRoom.isPublic ) ) ) @@ -150,7 +149,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( alias = peekResult.alias, membership = knownRoom?.membership ?: Membership.NONE, roomType = peekResult.roomType, - viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters + viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters, + isPublic = peekResult.isPublic ).also { peekResult.someMembers?.let { checkForKnownMembers(it) } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 04f72000fa..ad71d0b1b5 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -39,7 +39,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomType import javax.inject.Inject class MatrixToRoomSpaceFragment @Inject constructor( - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val spaceCardRenderer: SpaceCardRenderer ) : VectorBaseFragment() { private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel() @@ -78,12 +79,19 @@ class MatrixToRoomSpaceFragment @Inject constructor( when (val peek = item.invoke()) { is RoomInfoResult.FullInfo -> { val matrixItem = peek.roomItem + avatarRenderer.render(matrixItem, views.matrixToCardAvatar) if (peek.roomType == RoomType.SPACE) { views.matrixToBetaTag.isVisible = true - avatarRenderer.renderSpace(matrixItem, views.matrixToCardAvatar) + views.matrixToAccessImage.isVisible = true + if (peek.isPublic) { + views.matrixToAccessText.setTextOrHide(context?.getString(R.string.public_space)) + views.matrixToAccessImage.setImageResource(R.drawable.ic_public_room) + } else { + views.matrixToAccessText.setTextOrHide(context?.getString(R.string.private_space)) + views.matrixToAccessImage.setImageResource(R.drawable.ic_room_private) + } } else { views.matrixToBetaTag.isVisible = false - avatarRenderer.render(matrixItem, views.matrixToCardAvatar) } views.matrixToCardNameText.setTextOrHide(peek.name) views.matrixToCardAliasText.setTextOrHide(peek.alias) @@ -166,25 +174,12 @@ class MatrixToRoomSpaceFragment @Inject constructor( } } - val images = listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5) + listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5) .onEach { it.isGone = true } when (state.peopleYouKnow) { is Success -> { val someYouKnow = state.peopleYouKnow.invoke() - if (someYouKnow.isEmpty()) { - views.peopleYouMayKnowText.isVisible = false - } else { - someYouKnow.forEachIndexed { index, item -> - images[index].isVisible = true - avatarRenderer.render(item, images[index]) - } - views.peopleYouMayKnowText.setTextOrHide( - resources.getQuantityString(R.plurals.space_people_you_know, - someYouKnow.count(), - someYouKnow.count() - ) - ) - } + spaceCardRenderer.renderPeopleYouKnow(views, someYouKnow) } else -> { views.peopleYouMayKnowText.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt new file mode 100644 index 0000000000..e51490a59c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.matrixto + +import androidx.core.view.isGone +import androidx.core.view.isVisible +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.resources.StringProvider +import im.vector.app.databinding.FragmentMatrixToRoomSpaceCardBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod +import im.vector.app.features.home.room.detail.timeline.tools.linkify +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.user.model.User +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class SpaceCardRenderer @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider +) { + + fun render(spaceSummary: RoomSummary?, + peopleYouKnow: List, + matrixLinkCallback: TimelineEventController.UrlClickCallback?, + inCard: FragmentMatrixToRoomSpaceCardBinding) { + if (spaceSummary == null) { + inCard.matrixToCardContentVisibility.isVisible = false + inCard.matrixToCardButtonLoading.isVisible = true + } else { + inCard.matrixToCardContentVisibility.isVisible = true + inCard.matrixToCardButtonLoading.isVisible = false + avatarRenderer.render(spaceSummary.toMatrixItem(), inCard.matrixToCardAvatar) + inCard.matrixToCardNameText.text = spaceSummary.name + inCard.matrixToBetaTag.isVisible = true + inCard.matrixToCardAliasText.setTextOrHide(spaceSummary.canonicalAlias) + inCard.matrixToCardDescText.setTextOrHide(spaceSummary.topic.linkify(matrixLinkCallback)) + if (spaceSummary.isPublic) { + inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.public_space)) + inCard.matrixToAccessImage.isVisible = true + inCard.matrixToAccessImage.setImageResource(R.drawable.ic_public_room) + } else { + inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.private_space)) + inCard.matrixToAccessImage.isVisible = true + inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private) + } + val memberCount = spaceSummary.otherMemberIds.size + if (memberCount != 0) { + inCard.matrixToMemberPills.isVisible = true + inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) + } else { + // hide the pill + inCard.matrixToMemberPills.isVisible = false + } + + renderPeopleYouKnow(inCard, peopleYouKnow.map { it.toMatrixItem() }) + } + inCard.matrixToCardDescText.movementMethod = createLinkMovementMethod(object : TimelineEventController.UrlClickCallback { + override fun onUrlClicked(url: String, title: String): Boolean { + return false + } + + override fun onUrlLongClicked(url: String): Boolean { + // host.callback?.onUrlInTopicLongClicked(url) + return true + } + }) + } + + fun render(spaceChildInfo: SpaceChildInfo?, + peopleYouKnow: List, + matrixLinkCallback: TimelineEventController.UrlClickCallback?, + inCard: FragmentMatrixToRoomSpaceCardBinding) { + if (spaceChildInfo == null) { + inCard.matrixToCardContentVisibility.isVisible = false + inCard.matrixToCardButtonLoading.isVisible = true + } else { + inCard.matrixToCardContentVisibility.isVisible = true + inCard.matrixToCardButtonLoading.isVisible = false + avatarRenderer.render(spaceChildInfo.toMatrixItem(), inCard.matrixToCardAvatar) + inCard.matrixToCardNameText.setTextOrHide(spaceChildInfo.name) + inCard.matrixToBetaTag.isVisible = true + inCard.matrixToCardAliasText.setTextOrHide(spaceChildInfo.canonicalAlias) + inCard.matrixToCardDescText.setTextOrHide(spaceChildInfo.topic?.linkify(matrixLinkCallback)) + if (spaceChildInfo.worldReadable) { + inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.public_space)) + inCard.matrixToAccessImage.isVisible = true + inCard.matrixToAccessImage.setImageResource(R.drawable.ic_public_room) + } else { + inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.private_space)) + inCard.matrixToAccessImage.isVisible = true + inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private) + } + val memberCount = spaceChildInfo.activeMemberCount ?: 0 + if (memberCount != 0) { + inCard.matrixToMemberPills.isVisible = true + inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount) + } else { + // hide the pill + inCard.matrixToMemberPills.isVisible = false + } + + renderPeopleYouKnow(inCard, peopleYouKnow.map { it.toMatrixItem() }) + } + } + + fun renderPeopleYouKnow(inCard: FragmentMatrixToRoomSpaceCardBinding, peopleYouKnow: List) { + val images = listOf( + inCard.knownMember1, + inCard.knownMember2, + inCard.knownMember3, + inCard.knownMember4, + inCard.knownMember5 + ).onEach { it.isGone = true } + + if (peopleYouKnow.isEmpty()) { + inCard.peopleYouMayKnowText.isVisible = false + } else { + peopleYouKnow.forEachIndexed { index, item -> + images[index].isVisible = true + avatarRenderer.render(item, images[index]) + } + inCard.peopleYouMayKnowText.setTextOrHide( + stringProvider.getQuantityString(R.plurals.space_people_you_know, + peopleYouKnow.count(), + peopleYouKnow.count() + ) + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 9646142deb..ba700c0f0c 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -278,7 +278,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc val resolvedUrl = when (mode) { Mode.FULL_SIZE, Mode.ANIMATED_THUMBNAIL, - Mode.STICKER -> resolveUrl(data) + Mode.STICKER -> resolveUrl(data) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } // Fallback to base url @@ -357,7 +357,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc finalHeight = min(maxImageWidth * height / width, maxImageHeight) finalWidth = finalHeight * width / height } - Mode.STICKER -> { + Mode.STICKER -> { // limit on width val maxWidthDp = min(dimensionConverter.dpToPx(240), maxImageWidth) finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 3abf01583c..ed57ccb04c 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -65,6 +65,7 @@ import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinMode import im.vector.app.features.roomdirectory.RoomDirectoryActivity +import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData @@ -86,7 +87,6 @@ import im.vector.app.features.widgets.WidgetArgsBuilder import im.vector.app.space import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -129,7 +129,7 @@ class DefaultNavigator @Inject constructor( } appStateHandler.setCurrentSpace(spaceId) when (postSwitchSpaceAction) { - Navigator.PostSwitchSpaceAction.None -> { + Navigator.PostSwitchSpaceAction.None -> { // go back to home if we are showing room details? // This is a bit ugly, but the navigator is supposed to know about the activity stack if (context is RoomDetailActivity) { @@ -139,7 +139,7 @@ class DefaultNavigator @Inject constructor( Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> { startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false) } - is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { + is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { val args = RoomDetailArgs( postSwitchSpaceAction.roomId, eventId = null, @@ -278,7 +278,7 @@ class DefaultNavigator @Inject constructor( val intent = RoomDirectoryActivity.getIntent(context, initialFilter) context.startActivity(intent) } - is RoomGroupingMethod.BySpace -> { + is RoomGroupingMethod.BySpace -> { val selectedSpace = groupingMethod.space() if (selectedSpace == null) { val intent = RoomDirectoryActivity.getIntent(context, initialFilter) @@ -320,7 +320,7 @@ class DefaultNavigator @Inject constructor( val intent = InviteUsersToRoomActivity.getIntent(context, roomId) context.startActivity(intent) } - is RoomGroupingMethod.BySpace -> { + is RoomGroupingMethod.BySpace -> { if (currentGroupingMethod.spaceSummary != null) { // let user decides if he does it from space or room (context as? AppCompatActivity)?.supportFragmentManager?.let { fm -> diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 444c48bddb..cf0263a1e8 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -26,11 +26,11 @@ import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.login.LoginConfig import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinMode +import im.vector.app.features.roomdirectory.RoomDirectoryData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem @@ -44,7 +44,7 @@ interface Navigator { sealed class PostSwitchSpaceAction { object None : PostSwitchSpaceAction() data class OpenDefaultRoom(val roomId: String, val showShareSheet: Boolean) : PostSwitchSpaceAction() - object OpenAddExistingRooms: PostSwitchSpaceAction() + object OpenAddExistingRooms : PostSwitchSpaceAction() } fun switchToSpace(context: Context, spaceId: String, postSwitchSpaceAction: PostSwitchSpaceAction) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 1ccdbd26e4..648566f5f3 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -50,12 +50,12 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver -import org.matrix.android.sdk.api.session.call.MxCall import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -286,7 +286,7 @@ class NotificationUtils @Inject constructor(private val context: Context, * @return the call notification. */ @SuppressLint("NewApi") - fun buildIncomingCallNotification(mxCall: MxCall, + fun buildIncomingCallNotification(call: WebRtcCall, title: String, fromBg: Boolean): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) @@ -294,7 +294,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val builder = NotificationCompat.Builder(context, notificationChannel) .setContentTitle(ensureTitleNotEmpty(title)) .apply { - if (mxCall.isVideoCall) { + if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.incoming_video_call)) } else { setContentText(stringProvider.getString(R.string.incoming_voice_call)) @@ -307,11 +307,11 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentIntent = VectorCallActivity.newIntent( context = context, - mxCall = mxCall, + call = call, mode = VectorCallActivity.INCOMING_RINGING ).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://${mxCall.callId}") + data = Uri.parse("foobar://${call.callId}") } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) @@ -319,12 +319,12 @@ class NotificationUtils @Inject constructor(private val context: Context, .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(VectorCallActivity.newIntent( context = context, - mxCall = mxCall, + call = call, mode = VectorCallActivity.INCOMING_ACCEPT) ) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) - val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) + val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) builder.addAction( NotificationCompat.Action( @@ -350,7 +350,7 @@ class NotificationUtils @Inject constructor(private val context: Context, return builder.build() } - fun buildOutgoingRingingCallNotification(mxCall: MxCall, + fun buildOutgoingRingingCallNotification(call: WebRtcCall, title: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) @@ -365,14 +365,14 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentIntent = VectorCallActivity.newIntent( context = context, - mxCall = mxCall, + call = call, mode = null).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://$mxCall.callId") + data = Uri.parse("foobar://$call.callId") } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) - val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) + val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) builder.addAction( NotificationCompat.Action( @@ -396,12 +396,12 @@ class NotificationUtils @Inject constructor(private val context: Context, * @return the call notification. */ @SuppressLint("NewApi") - fun buildPendingCallNotification(mxCall: MxCall, + fun buildPendingCallNotification(call: WebRtcCall, title: String): Notification { val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(ensureTitleNotEmpty(title)) .apply { - if (mxCall.isVideoCall) { + if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.video_call_in_progress)) } else { setContentText(stringProvider.getString(R.string.call_in_progress)) @@ -410,7 +410,7 @@ class NotificationUtils @Inject constructor(private val context: Context, .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) - val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) + val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) builder.addAction( NotificationCompat.Action( @@ -421,7 +421,7 @@ class NotificationUtils @Inject constructor(private val context: Context, val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) - .addNextIntent(VectorCallActivity.newIntent(context, mxCall, null)) + .addNextIntent(VectorCallActivity.newIntent(context, call, null)) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(contentPendingIntent) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt index 16e5428b9c..fdab72caba 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt @@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData data class PublicRoomsViewState( // The current filter diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt index a94cb7709f..77eec57ab3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt @@ -17,7 +17,6 @@ package im.vector.app.features.roomdirectory import im.vector.app.core.platform.VectorViewModelAction -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData sealed class RoomDirectoryAction : VectorViewModelAction { data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index d8edbcf503..9a63e81a2f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.extensions.popBackstack import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment @@ -58,7 +59,7 @@ class RoomDirectoryActivity : VectorBaseActivity() { .observe() .subscribe { sharedAction -> when (sharedAction) { - is RoomDirectorySharedAction.Back -> onBackPressed() + is RoomDirectorySharedAction.Back -> popBackstack() is RoomDirectorySharedAction.CreateRoom -> { // Transmit the filter to the CreateRoomFragment withState(roomDirectoryViewModel) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt similarity index 75% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt rename to vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt index 91f429d773..49bb769460 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.room.model.thirdparty +package im.vector.app.features.roomdirectory /** - * This class describes a rooms directory server. + * This class describes a rooms directory server protocol. */ data class RoomDirectoryData( - /** * The server name (might be null) * Set null when the server is the current user's home server. @@ -30,7 +29,12 @@ data class RoomDirectoryData( /** * The display name (the server description) */ - val displayName: String = DEFAULT_HOME_SERVER_NAME, + val displayName: String = MATRIX_PROTOCOL_NAME, + + /** + * the avatar url + */ + val avatarUrl: String? = null, /** * The third party server identifier @@ -40,15 +44,10 @@ data class RoomDirectoryData( /** * Tell if all the federated servers must be included */ - val includeAllNetworks: Boolean = false, - - /** - * the avatar url - */ - val avatarUrl: String? = null + val includeAllNetworks: Boolean = false ) { companion object { - const val DEFAULT_HOME_SERVER_NAME = "Matrix" + const val MATRIX_PROTOCOL_NAME = "Matrix" } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt new file mode 100644 index 0000000000..0f29ae5986 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory + +data class RoomDirectoryServer( + val serverName: String, + + /** + * True if this is the current user server + */ + val isUserServer: Boolean, + + /** + * True if manually added, so it can be removed by the user + */ + val isManuallyAdded: Boolean, + + /** + * Supported protocols + * TODO Rename RoomDirectoryData to RoomDirectoryProtocols + */ + val protocols: List +) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index f64105b759..dc1cbfc58d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.rx.rx import timber.log.Timber @@ -230,9 +229,7 @@ class RoomDirectoryViewModel @AssistedInject constructor( Timber.w("Try to join an already joining room. Should not happen") return@withState } - val viaServers = state.roomDirectoryData.homeServer - ?.let { listOf(it) } - .orEmpty() + val viaServers = listOfNotNull(state.roomDirectoryData.homeServer) viewModelScope.launch { try { session.joinRoom(action.roomId, viaServers = viaServers) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 03c98d249c..3a4f65b50b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -79,6 +79,7 @@ class CreateRoomController @Inject constructor( id("topic") enabled(enableFormElement) value(viewState.roomTopic) + singleLine(false) hint(host.stringProvider.getString(R.string.create_room_topic_hint)) onTextChange { text -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt index 7b2e329b6a..7cf8e538ac 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt @@ -16,10 +16,12 @@ package im.vector.app.features.roomdirectory.picker +import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.isInvisible +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R @@ -43,6 +45,9 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() @EpoxyAttribute var includeAllNetworks: Boolean = false + @EpoxyAttribute + var checked: Boolean = false + @EpoxyAttribute var globalListener: (() -> Unit)? = null @@ -63,6 +68,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() holder.nameView.text = directoryName holder.descriptionView.setTextOrHide(directoryDescription) + holder.checkedView.isVisible = checked } class Holder : VectorEpoxyHolder() { @@ -71,5 +77,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() val avatarView by bind(R.id.itemRoomDirectoryAvatar) val nameView by bind(R.id.itemRoomDirectoryName) val descriptionView by bind(R.id.itemRoomDirectoryDescription) + val checkedView by bind(R.id.itemRoomDirectoryChecked) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt index d51ad5040b..65d8f2d1cb 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt @@ -18,55 +18,110 @@ package im.vector.app.features.roomdirectory.picker import im.vector.app.R import im.vector.app.core.resources.StringArrayProvider +import im.vector.app.features.roomdirectory.RoomDirectoryData +import im.vector.app.features.roomdirectory.RoomDirectoryServer import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import javax.inject.Inject -class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvider: StringArrayProvider, - private val session: Session) { +class RoomDirectoryListCreator @Inject constructor( + private val stringArrayProvider: StringArrayProvider, + private val session: Session +) { - fun computeDirectories(thirdPartyProtocolData: Map): List { - val result = ArrayList() + fun computeDirectories(thirdPartyProtocolData: Map, + customHomeservers: Set): List { + val result = ArrayList() + + val protocols = ArrayList() // Add user homeserver name val userHsName = session.myUserId.substringAfter(":") - result.add(RoomDirectoryData( - displayName = userHsName, - includeAllNetworks = true - )) - - // Add user's HS but for Matrix public rooms only - result.add(RoomDirectoryData()) - - // Add custom directory servers - val hsNamesList = stringArrayProvider.getStringArray(R.array.room_directory_servers) - hsNamesList.forEach { - if (it != userHsName) { - // Use the server name as a default display name - result.add(RoomDirectoryData( - homeServer = it, - displayName = it, - includeAllNetworks = true - )) - } - } + // Add default protocol + protocols.add( + RoomDirectoryData( + homeServer = null, + displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME, + includeAllNetworks = false + ) + ) // Add result of the request thirdPartyProtocolData.forEach { it.value.instances?.forEach { thirdPartyProtocolInstance -> - result.add(RoomDirectoryData( - homeServer = null, - displayName = thirdPartyProtocolInstance.desc ?: "", - thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId, - includeAllNetworks = false, - // Default to protocol icon - avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon - )) + protocols.add( + RoomDirectoryData( + homeServer = null, + displayName = thirdPartyProtocolInstance.desc ?: "", + thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId, + includeAllNetworks = false, + // Default to protocol icon + avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon + ) + ) } } + // Add all rooms + protocols.add( + RoomDirectoryData( + homeServer = null, + displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME, + includeAllNetworks = true + ) + ) + + result.add( + RoomDirectoryServer( + serverName = userHsName, + isUserServer = true, + isManuallyAdded = false, + protocols = protocols + ) + ) + + // Add custom directory servers, form the config file, excluding the current user homeserver + stringArrayProvider.getStringArray(R.array.room_directory_servers) + .filter { it != userHsName } + .forEach { + // Use the server name as a default display name + result.add( + RoomDirectoryServer( + serverName = it, + isUserServer = false, + isManuallyAdded = false, + protocols = listOf( + RoomDirectoryData( + homeServer = it, + displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME, + includeAllNetworks = false + ) + ) + ) + ) + } + + // Add manually added server by the user + customHomeservers + .forEach { + // Use the server name as a default display name + result.add( + RoomDirectoryServer( + serverName = it, + isUserServer = false, + isManuallyAdded = true, + protocols = listOf( + RoomDirectoryData( + homeServer = it, + displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME, + includeAllNetworks = false + ) + ) + ) + ) + } + return result } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt index 36f2cd4296..8be3c6b2b2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt @@ -17,7 +17,14 @@ package im.vector.app.features.roomdirectory.picker import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.roomdirectory.RoomDirectoryServer sealed class RoomDirectoryPickerAction : VectorViewModelAction { object Retry : RoomDirectoryPickerAction() + object EnterEditMode : RoomDirectoryPickerAction() + object ExitEditMode : RoomDirectoryPickerAction() + data class SetServerUrl(val url: String) : RoomDirectoryPickerAction() + data class RemoveServer(val roomDirectoryServer: RoomDirectoryServer) : RoomDirectoryPickerAction() + + object Submit : RoomDirectoryPickerAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt index 75e9807bd0..9a397c586d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt @@ -16,37 +16,62 @@ package im.vector.app.features.roomdirectory.picker +import android.text.InputType +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import im.vector.app.R +import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.join +import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData +import im.vector.app.core.ui.list.genericButtonItem +import im.vector.app.core.ui.list.verticalMarginItem +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.discovery.settingsContinueCancelItem +import im.vector.app.features.discovery.settingsInformationItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.roomdirectory.RoomDirectoryData +import im.vector.app.features.roomdirectory.RoomDirectoryServer +import org.matrix.android.sdk.api.failure.Failure import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection -class RoomDirectoryPickerController @Inject constructor(private val stringProvider: StringProvider, - private val errorFormatter: ErrorFormatter, - private val roomDirectoryListCreator: RoomDirectoryListCreator +class RoomDirectoryPickerController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, + private val errorFormatter: ErrorFormatter ) : TypedEpoxyController() { + var currentRoomDirectoryData: RoomDirectoryData? = null var callback: Callback? = null - var index = 0 + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) - override fun buildModels(viewState: RoomDirectoryPickerViewState) { + override fun buildModels(data: RoomDirectoryPickerViewState) { val host = this - val asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest - when (asyncThirdPartyProtocol) { + when (val asyncThirdPartyProtocol = data.asyncThirdPartyRequest) { is Success -> { - val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol()) - - directories.forEach { - buildDirectory(it) + data.directories.join( + each = { _, roomDirectoryServer -> buildDirectory(roomDirectoryServer) }, + between = { idx, _ -> buildDivider(idx) } + ) + buildForm(data) + verticalMarginItem { + id("space_bottom") + heightInPx(host.dimensionConverter.dpToPx(16)) } } is Incomplete -> { @@ -64,28 +89,131 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid } } - private fun buildDirectory(roomDirectoryData: RoomDirectoryData) { + private fun buildForm(data: RoomDirectoryPickerViewState) { + buildDivider(1000) val host = this - roomDirectoryItem { - id(host.index++) - - directoryName(roomDirectoryData.displayName) - - val description = when { - roomDirectoryData.includeAllNetworks -> - host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryData.displayName) - "Matrix" == roomDirectoryData.displayName -> - host.stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryData.displayName) - else -> - null + if (data.inEditMode) { + verticalMarginItem { + id("form_space") + heightInPx(host.dimensionConverter.dpToPx(16)) } + settingsInformationItem { + id("form_notice") + message(host.stringProvider.getString(R.string.directory_add_a_new_server_prompt)) + colorProvider(host.colorProvider) + } + verticalMarginItem { + id("form_space_2") + heightInPx(host.dimensionConverter.dpToPx(8)) + } + formEditTextItem { + id("edit") + showBottomSeparator(false) + value(data.enteredServer) + imeOptions(EditorInfo.IME_ACTION_DONE) + editorActionListener(object : TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE) { + if (data.enteredServer.isNotEmpty()) { + host.callback?.onSubmitServer() + } + return true + } + return false + } + }) + hint(host.stringProvider.getString(R.string.directory_server_placeholder)) + inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI) + onTextChange { text -> + host.callback?.onEnterServerChange(text) + } + when (data.addServerAsync) { + Uninitialized -> enabled(true) + is Loading -> enabled(false) + is Success -> enabled(false) + is Fail -> { + enabled(true) + errorMessage(host.getErrorMessage(data.addServerAsync.error)) + } + } + } + when (data.addServerAsync) { + Uninitialized, + is Fail -> settingsContinueCancelItem { + id("continueCancel") + continueText(host.stringProvider.getString(R.string.ok)) + canContinue(data.enteredServer.isNotEmpty()) + continueOnClick { host.callback?.onSubmitServer() } + cancelOnClick { host.callback?.onCancelEnterServer() } + } + is Loading -> loadingItem { + id("addLoading") + } + is Success -> Unit /* This is a transitive state */ + } + } else { + genericButtonItem { + id("add") + text(host.stringProvider.getString(R.string.directory_add_a_new_server)) + textColor(host.colorProvider.getColor(R.color.riotx_accent)) + buttonClickAction(View.OnClickListener { + host.callback?.onStartEnterServer() + }) + } + } + } - directoryDescription(description) - directoryAvatarUrl(roomDirectoryData.avatarUrl) - includeAllNetworks(roomDirectoryData.includeAllNetworks) + private fun getErrorMessage(error: Throwable): String { + return if (error is Failure.ServerError + && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + stringProvider.getString(R.string.directory_add_a_new_server_error) + } else { + errorFormatter.toHumanReadable(error) + } + } - globalListener { - host.callback?.onRoomDirectoryClicked(roomDirectoryData) + private fun buildDivider(idx: Int) { + val host = this + dividerItem { + id("divider_$idx") + color(host.dividerColor) + } + } + + private fun buildDirectory(roomDirectoryServer: RoomDirectoryServer) { + val host = this + roomDirectoryServerItem { + id("server_$roomDirectoryServer") + serverName(roomDirectoryServer.serverName) + canRemove(roomDirectoryServer.isManuallyAdded) + removeListener { host.callback?.onRemoveServer(roomDirectoryServer) } + + if (roomDirectoryServer.isUserServer) { + serverDescription(host.stringProvider.getString(R.string.directory_your_server)) + } + } + + roomDirectoryServer.protocols.forEach { roomDirectoryData -> + roomDirectoryItem { + id("server_${roomDirectoryServer}_proto_$roomDirectoryData") + directoryName( + if (roomDirectoryData.includeAllNetworks) { + host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryServer.serverName) + } else { + roomDirectoryData.displayName + } + ) + if (roomDirectoryData.displayName == RoomDirectoryData.MATRIX_PROTOCOL_NAME && !roomDirectoryData.includeAllNetworks) { + directoryDescription( + host.stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryServer.serverName) + ) + } + directoryAvatarUrl(roomDirectoryData.avatarUrl) + includeAllNetworks(roomDirectoryData.includeAllNetworks) + checked(roomDirectoryData == host.currentRoomDirectoryData) + globalListener { + host.callback?.onRoomDirectoryClicked(roomDirectoryData) + } } } } @@ -93,5 +221,10 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid interface Callback { fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) fun retry() + fun onStartEnterServer() + fun onEnterServerChange(server: String) + fun onSubmitServer() + fun onCancelEnterServer() + fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index 7f205078f1..a32a3a897f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -18,7 +18,6 @@ package im.vector.app.features.roomdirectory.picker import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity @@ -28,21 +27,22 @@ import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding import im.vector.app.features.roomdirectory.RoomDirectoryAction +import im.vector.app.features.roomdirectory.RoomDirectoryData +import im.vector.app.features.roomdirectory.RoomDirectoryServer import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.RoomDirectoryViewModel - -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import timber.log.Timber import javax.inject.Inject -// TODO Menu to add custom room directory (not done in RiotWeb so far...) class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory, private val roomDirectoryPickerController: RoomDirectoryPickerController ) : VectorBaseFragment(), + OnBackPressed, RoomDirectoryPickerController.Callback { private val viewModel: RoomDirectoryViewModel by activityViewModel() @@ -65,6 +65,11 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() + + // Give the current data to our controller. There maybe a better way to do that... + withState(viewModel) { + roomDirectoryPickerController.currentRoomDirectoryData = it.roomDirectoryData + } } override fun onDestroyView() { @@ -73,18 +78,6 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie super.onDestroyView() } - override fun getMenuRes() = R.menu.menu_directory_server_picker - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.action_add_custom_hs) { - // TODO - vectorBaseActivity.notImplemented("Entering custom homeserver") - return true - } - - return super.onOptionsItemSelected(item) - } - private fun setupRecyclerView() { views.roomDirectoryPickerList.configureWith(roomDirectoryPickerController) roomDirectoryPickerController.callback = this @@ -97,6 +90,26 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie sharedActionViewModel.post(RoomDirectorySharedAction.Back) } + override fun onStartEnterServer() { + pickerViewModel.handle(RoomDirectoryPickerAction.EnterEditMode) + } + + override fun onCancelEnterServer() { + pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode) + } + + override fun onEnterServerChange(server: String) { + pickerViewModel.handle(RoomDirectoryPickerAction.SetServerUrl(server)) + } + + override fun onSubmitServer() { + pickerViewModel.handle(RoomDirectoryPickerAction.Submit) + } + + override fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer) { + pickerViewModel.handle(RoomDirectoryPickerAction.RemoveServer(roomDirectoryServer)) + } + override fun onResume() { super.onResume() (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory) @@ -111,4 +124,16 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie // Populate list with Epoxy roomDirectoryPickerController.setData(state) } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + // Leave the add server mode if started + return withState(pickerViewModel) { + if (it.inEditMode) { + pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode) + true + } else { + false + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index d85b7937a2..2558715834 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -22,18 +22,28 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams -class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState, - private val session: Session) - : VectorViewModel(initialState) { +class RoomDirectoryPickerViewModel @AssistedInject constructor( + @Assisted initialState: RoomDirectoryPickerViewState, + private val session: Session, + private val uiStateRepository: UiStateRepository, + private val stringProvider: StringProvider, + private val roomDirectoryListCreator: RoomDirectoryListCreator +) : VectorViewModel(initialState) { @AssistedFactory interface Factory { @@ -50,7 +60,22 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial } init { + observeAndCompute() load() + loadCustomRoomDirectoryHomeservers() + } + + private fun observeAndCompute() { + selectSubscribe( + RoomDirectoryPickerViewState::asyncThirdPartyRequest, + RoomDirectoryPickerViewState::customHomeservers + ) { async, custom -> + async()?.let { + setState { + copy(directories = roomDirectoryListCreator.computeDirectories(it, custom)) + } + } + } } private fun load() { @@ -71,9 +96,101 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial } } + private fun loadCustomRoomDirectoryHomeservers() { + setState { + copy( + customHomeservers = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) + ) + } + } + override fun handle(action: RoomDirectoryPickerAction) { when (action) { - RoomDirectoryPickerAction.Retry -> load() + RoomDirectoryPickerAction.Retry -> load() + RoomDirectoryPickerAction.EnterEditMode -> handleEnterEditMode() + RoomDirectoryPickerAction.ExitEditMode -> handleExitEditMode() + is RoomDirectoryPickerAction.SetServerUrl -> handleSetServerUrl(action) + RoomDirectoryPickerAction.Submit -> handleSubmit() + is RoomDirectoryPickerAction.RemoveServer -> handleRemoveServer(action) + }.exhaustive + } + + private fun handleEnterEditMode() { + setState { + copy( + inEditMode = true, + enteredServer = "", + addServerAsync = Uninitialized + ) + } + } + + private fun handleExitEditMode() { + setState { + copy( + inEditMode = false, + enteredServer = "", + addServerAsync = Uninitialized + ) + } + } + + private fun handleSetServerUrl(action: RoomDirectoryPickerAction.SetServerUrl) { + setState { + copy( + enteredServer = action.url + ) + } + } + + private fun handleSubmit() = withState { state -> + // First avoid duplicate + val enteredServer = state.enteredServer + + val existingServerList = state.directories.map { it.serverName } + + if (enteredServer in existingServerList) { + setState { + copy(addServerAsync = Fail(Throwable(stringProvider.getString(R.string.directory_add_a_new_server_error_already_added)))) + } + return@withState + } + + viewModelScope.launch { + setState { + copy(addServerAsync = Loading()) + } + try { + session.getPublicRooms( + server = enteredServer, + publicRoomsParams = PublicRoomsParams(limit = 1) + ) + // Success, let add the server to our local repository, and update the state + val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) + enteredServer + uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet) + setState { + copy( + inEditMode = false, + enteredServer = "", + addServerAsync = Uninitialized, + customHomeservers = newSet + ) + } + } catch (failure: Throwable) { + setState { + copy(addServerAsync = Fail(failure)) + } + } + } + } + + private fun handleRemoveServer(action: RoomDirectoryPickerAction.RemoveServer) { + val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) - action.roomDirectoryServer.serverName + uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet) + setState { + copy( + customHomeservers = newSet + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt index 61cf50e8dd..5cdee862ab 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt @@ -19,8 +19,15 @@ package im.vector.app.features.roomdirectory.picker import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.roomdirectory.RoomDirectoryServer import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol data class RoomDirectoryPickerViewState( - val asyncThirdPartyRequest: Async> = Uninitialized + val asyncThirdPartyRequest: Async> = Uninitialized, + val customHomeservers: Set = emptySet(), + val inEditMode: Boolean = false, + val enteredServer: String = "", + val addServerAsync: Async = Uninitialized, + // computed + val directories: List = emptyList() ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt new file mode 100644 index 0000000000..6efb41d5b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory.picker + +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_room_directory_server) +abstract class RoomDirectoryServerItem : VectorEpoxyModel() { + + @EpoxyAttribute + var serverName: String? = null + + @EpoxyAttribute + var serverDescription: String? = null + + @EpoxyAttribute + var canRemove: Boolean = false + + @EpoxyAttribute + var removeListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.nameView.text = serverName + holder.descriptionView.setTextOrHide(serverDescription) + holder.deleteView.isVisible = canRemove + holder.deleteView.onClick(removeListener) + } + + class Holder : VectorEpoxyHolder() { + val nameView by bind(R.id.itemRoomDirectoryServerName) + val descriptionView by bind(R.id.itemRoomDirectoryServerDescription) + val deleteView by bind(R.id.itemRoomDirectoryServerRemove) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 445d02d6e2..f9cf8e6dd7 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -25,9 +25,9 @@ import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.roomdirectory.RoomDirectoryData import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom -import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 24836bc504..6ee26ed54f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -68,16 +68,14 @@ class RoomSettingsController @Inject constructor( id("avatar") enabled(data.actionPermissions.canChangeAvatar) when (val avatarAction = data.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { // Use the current value avatarRenderer(host.avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. - matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) + matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> - imageUri(null) - is RoomSettingsViewState.AvatarAction.UpdateAvatar -> - imageUri(avatarAction.newAvatarUri) + RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null) + is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) } clickListener { host.callback?.onAvatarChange() } deleteListener { host.callback?.onAvatarDelete() } @@ -102,6 +100,7 @@ class RoomSettingsController @Inject constructor( id("topic") enabled(data.actionPermissions.canChangeTopic) value(data.newTopic ?: roomSummary.topic) + singleLine(false) hint(host.stringProvider.getString(R.string.room_settings_topic_hint)) onTextChange { text -> diff --git a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt index 7b7be550cb..d07e26d82d 100644 --- a/vector/src/main/java/im/vector/app/features/session/SessionListener.kt +++ b/vector/src/main/java/im/vector/app/features/session/SessionListener.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.MutableLiveData import im.vector.app.core.extensions.postLiveEvent import im.vector.app.core.utils.LiveEvent import kotlinx.coroutines.cancelChildren +import im.vector.app.features.call.vectorCallService +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.session.Session import javax.inject.Inject @@ -37,6 +39,12 @@ class SessionListener @Inject constructor() : Session.Listener { _globalErrorLiveData.postLiveEvent(globalError) } + override fun onNewInvitedRoom(session: Session, roomId: String) { + session.coroutineScope.launch { + session.vectorCallService.userMapper.onNewInvitedRoom(roomId) + } + } + override fun onSessionStopped(session: Session) { session.coroutineScope.coroutineContext.cancelChildren() } diff --git a/vector/src/main/java/im/vector/app/features/session/SessionScopedProperty.kt b/vector/src/main/java/im/vector/app/features/session/SessionScopedProperty.kt new file mode 100644 index 0000000000..e6a84a41d8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/session/SessionScopedProperty.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.session + +import org.matrix.android.sdk.api.session.Session +import kotlin.reflect.KProperty + +/** + * This is a simple hack for having some Session scope dependencies. + * Probably a temporary solution waiting for refactoring the Dagger management of Session. + * You should use it with an extension property : + val Session.myProperty: MyProperty by SessionScopedProperty { + init code + } + * + */ +class SessionScopedProperty(val initializer: (Session) -> T) { + + private val propertyBySessionId = HashMap() + + private val sessionListener = object : Session.Listener { + + override fun onSessionStopped(session: Session) { + synchronized(propertyBySessionId) { + session.removeListener(this) + propertyBySessionId.remove(session.sessionId) + } + } + } + + operator fun getValue(thisRef: Session, property: KProperty<*>): T = synchronized(propertyBySessionId) { + propertyBySessionId.getOrPut(thisRef.sessionId) { + thisRef.addListener(sessionListener) + initializer(thisRef) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt index 8f4e36b9a1..7636ecac6a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataEpoxyController.kt @@ -27,7 +27,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemWithValue import im.vector.app.core.utils.DebouncedClickListener -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import javax.inject.Inject class AccountDataEpoxyController @Inject constructor( @@ -35,8 +35,8 @@ class AccountDataEpoxyController @Inject constructor( ) : TypedEpoxyController() { interface InteractionListener { - fun didTap(data: UserAccountDataEvent) - fun didLongTap(data: UserAccountDataEvent) + fun didTap(data: AccountDataEvent) + fun didLongTap(data: AccountDataEvent) } var interactionListener: InteractionListener? = null diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt index c50dd3c187..df6c8bd5fa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataFragment.kt @@ -35,7 +35,7 @@ import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding import org.billcarsonfr.jsonviewer.JSonViewerDialog -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.internal.di.MoshiProvider import javax.inject.Inject @@ -73,9 +73,9 @@ class AccountDataFragment @Inject constructor( super.onDestroyView() } - override fun didTap(data: UserAccountDataEvent) { + override fun didTap(data: AccountDataEvent) { val jsonString = MoshiProvider.providesMoshi() - .adapter(UserAccountDataEvent::class.java) + .adapter(AccountDataEvent::class.java) .toJson(data) JSonViewerDialog.newInstance( jsonString, @@ -84,7 +84,7 @@ class AccountDataFragment @Inject constructor( ).show(childFragmentManager, "JSON_VIEWER") } - override fun didLongTap(data: UserAccountDataEvent) { + override fun didLongTap(data: AccountDataEvent) { AlertDialog.Builder(requireActivity()) .setTitle(R.string.delete) .setMessage(getString(R.string.delete_account_data_warning, data.type)) diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt index 7880e734a5..421bc53396 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt @@ -31,11 +31,11 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.rx.rx data class AccountDataViewState( - val accountData: Async> = Uninitialized + val accountData: Async> = Uninitialized ) : MvRxState class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState, @@ -43,7 +43,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A : VectorViewModel(initialState) { init { - session.rx().liveAccountData(emptySet()) + session.rx().liveUserAccountData(emptySet()) .execute { copy(accountData = it) } @@ -57,7 +57,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) { viewModelScope.launch { - session.updateAccountData(action.type, emptyMap()) + session.userAccountDataService().updateAccountData(action.type, emptyMap()) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 2526b62f39..6cf3369267 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -96,7 +96,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { holder.indentSpace.isVisible = indent > 0 holder.separator.isVisible = showSeparator - avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) holder.counterBadgeView.render(countState) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt index bd6bee068d..0728168898 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt @@ -81,7 +81,7 @@ abstract class SubSpaceSummaryItem : VectorEpoxyModel diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index e334868d7c..e4ed431e73 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -122,13 +122,15 @@ class SpaceDirectoryController @Inject constructor( val isSpace = info.roomType == RoomType.SPACE val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false + // if it's known use that matrixItem because it would have a better computed name + val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem() + ?: info.toMatrixItem() spaceChildInfoItem { id(info.childRoomId) - matrixItem(info.toMatrixItem()) + matrixItem(matrixItem) avatarRenderer(host.avatarRenderer) topic(info.topic) memberCount(info.activeMemberCount ?: 0) - space(isSpace) loading(isLoading) buttonLabel( if (isJoined) host.stringProvider.getString(R.string.action_open) diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index a866ea9b89..910c21926c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.spaces.explore +import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -23,19 +24,33 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.text.toSpannable +import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import im.vector.app.R +import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.colorizeMatchingText +import im.vector.app.core.utils.isValidUrl +import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.matrixto.SpaceCardRenderer +import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import java.net.URL import javax.inject.Inject @Parcelize @@ -44,9 +59,13 @@ data class SpaceDirectoryArgs( ) : Parcelable class SpaceDirectoryFragment @Inject constructor( - private val epoxyController: SpaceDirectoryController + private val epoxyController: SpaceDirectoryController, + private val permalinkHandler: PermalinkHandler, + private val spaceCardRenderer: SpaceCardRenderer, + private val colorProvider: ColorProvider ) : VectorBaseFragment(), SpaceDirectoryController.InteractionListener, + TimelineEventController.UrlClickCallback, OnBackPressed { override fun getMenuRes() = R.menu.menu_space_directory @@ -71,6 +90,9 @@ class SpaceDirectoryFragment @Inject constructor( viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) { invalidateOptionsMenu() } + + views.spaceCard.matrixToCardMainButton.isVisible = false + views.spaceCard.matrixToCardSecondaryButton.isVisible = false } override fun onDestroyView() { @@ -82,10 +104,21 @@ class SpaceDirectoryFragment @Inject constructor( override fun invalidate() = withState(viewModel) { state -> epoxyController.setData(state) - val title = state.hierarchyStack.lastOrNull()?.let { currentParent -> + val currentParent = state.hierarchyStack.lastOrNull()?.let { currentParent -> state.spaceSummaryApiResult.invoke()?.firstOrNull { it.childRoomId == currentParent } - }?.name ?: getString(R.string.space_explore_activity_title) - views.toolbar.title = title + } + + if (currentParent == null) { + val title = getString(R.string.space_explore_activity_title) + views.toolbar.title = title + + spaceCardRenderer.render(state.spaceSummary.invoke(), emptyList(), this, views.spaceCard) + } else { + val title = currentParent.name ?: currentParent.canonicalAlias ?: getString(R.string.space_explore_activity_title) + views.toolbar.title = title + + spaceCardRenderer.render(currentParent, emptyList(), this, views.spaceCard) + } } override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> @@ -96,7 +129,7 @@ class SpaceDirectoryFragment @Inject constructor( override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.spaceAddRoom -> { + R.id.spaceAddRoom -> { withState(viewModel) { state -> addExistingRooms(state.spaceId) } @@ -138,6 +171,44 @@ class SpaceDirectoryFragment @Inject constructor( override fun addExistingRooms(spaceId: String) { addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) } + + override fun onUrlClicked(url: String, title: String): Boolean { + permalinkHandler + .launch(requireActivity(), url, null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { managed -> + if (!managed) { + if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.external_link_confirmation_title) + .setMessage( + getString(R.string.external_link_confirmation_message, title, url) + .toSpannable() + .colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast)) + .colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast)) + ) + .setPositiveButton(R.string._continue) { _, _ -> + openUrlInExternalBrowser(requireContext(), url) + } + .setNegativeButton(R.string.cancel, null) + .show() + .withColoredButton(DialogInterface.BUTTON_NEGATIVE) + } else { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) + } + } + } + .disposeOnDestroyView() + // In fact it is always managed + return true + } + + override fun onUrlLongClicked(url: String): Boolean { + // nothing? + return false + } // override fun navigateToRoom(roomId: String) { // viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId)) // } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt index 220c3e3492..21541a51ab 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt @@ -37,7 +37,9 @@ data class SpaceDirectoryState( val joinedRoomsIds: Set = emptySet(), // keys are room alias or roomId val changeMembershipStates: Map = emptyMap(), - val canAddRooms: Boolean = false + val canAddRooms: Boolean = false, + // cached room summaries of known rooms + val knownRoomSummaries : List = emptyList() ) : MvRxState { constructor(args: SpaceDirectoryArgs) : this( spaceId = args.spaceId diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 313ddfe1dc..3d3e1dac65 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -66,7 +66,8 @@ class SpaceDirectoryViewModel @AssistedInject constructor( val spaceSum = session.getRoomSummary(initialState.spaceId) setState { copy( - childList = spaceSum?.spaceChildren ?: emptyList() + childList = spaceSum?.spaceChildren ?: emptyList(), + spaceSummary = spaceSum?.let { Success(spaceSum) } ?: Loading() ) } @@ -101,9 +102,14 @@ class SpaceDirectoryViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { try { val query = session.spaceService().querySpaceChildren(initialState.spaceId) + val knownSummaries = query.second.mapNotNull { + session.getRoomSummary(it.childRoomId) + ?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced) + } setState { copy( - spaceSummaryApiResult = Success(query.second) + spaceSummaryApiResult = Success(query.second), + knownRoomSummaries = knownSummaries ) } } catch (failure: Throwable) { @@ -148,7 +154,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor( copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId)) } } - SpaceDirectoryViewAction.HandleBack -> { + SpaceDirectoryViewAction.HandleBack -> { withState { if (it.hierarchyStack.isEmpty()) { _viewEvents.post(SpaceDirectoryViewEvents.Dismiss) @@ -161,20 +167,20 @@ class SpaceDirectoryViewModel @AssistedInject constructor( } } } - is SpaceDirectoryViewAction.JoinOrOpen -> { + is SpaceDirectoryViewAction.JoinOrOpen -> { handleJoinOrOpen(action.spaceChildInfo) } - is SpaceDirectoryViewAction.NavigateToRoom -> { + is SpaceDirectoryViewAction.NavigateToRoom -> { _viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId)) } - is SpaceDirectoryViewAction.ShowDetails -> { + is SpaceDirectoryViewAction.ShowDetails -> { // This is temporary for now to at least display something for the space beta // It's not ideal as it's doing some peeking that is not needed. session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let { _viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it)) } } - SpaceDirectoryViewAction.Retry -> { + SpaceDirectoryViewAction.Retry -> { refreshFromApi() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt index 8e536459a3..434fa613ab 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt @@ -22,7 +22,6 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -33,12 +32,12 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ScreenComponent -import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.ButtonStateView import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.utils.toast import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.matrixto.SpaceCardRenderer import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -60,6 +59,9 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment - images[index].isVisible = true - avatarRenderer.render(item.toMatrixItem(), images[index]) - } - views.spaceCard.peopleYouMayKnowText.setTextOrHide( - resources.getQuantityString(R.plurals.space_people_you_know, - peopleYouKnow.count(), - peopleYouKnow.count() - ) - ) - } } override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetInvitedToSpaceBinding { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt index 69be246506..e0504d6531 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt @@ -27,7 +27,6 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.list.RoomCategoryItem_ import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -155,7 +154,6 @@ class AddRoomListController @Inject constructor( id(item.roomId) matrixItem(item.toMatrixItem()) avatarRenderer(host.avatarRenderer) - space(item.roomType == RoomType.SPACE) selected(host.selectedItems[item.roomId] ?: false) itemClickListener(DebouncedClickListener({ host.listener?.onItemSelected(item) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt index 9d188ac457..50e92c8758 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt @@ -34,18 +34,14 @@ abstract class RoomManageSelectionItem : VectorEpoxyModel() @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem - @EpoxyAttribute var space: Boolean = false @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - if (space) { - avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) - } else { - avatarRenderer.render(matrixItem, holder.avatarImageView) - } + avatarRenderer.render(matrixItem, holder.avatarImageView) + holder.titleText.text = matrixItem.getBestName() if (selected) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt index b16c6de921..f740938ee4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -27,7 +27,6 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -83,7 +82,6 @@ class SpaceManageRoomsController @Inject constructor( matrixItem(childInfo.toMatrixItem()) avatarRenderer(host.avatarRenderer) suggested(childInfo.suggested ?: false) - space(childInfo.roomType == RoomType.SPACE) selected(data.selectedRooms.contains(childInfo.childRoomId)) itemClickListener(DebouncedClickListener({ host.listener?.toggleSelection(childInfo) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt index 614f6f92c8..e0e7575f35 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -71,7 +71,7 @@ class SpaceSettingsController @Inject constructor( // Use the current value avatarRenderer(host.avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. - matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) + matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl)) } RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 85c73ac8ef..7b405eb4f3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -139,7 +139,7 @@ class SpaceSettingsFragment @Inject constructor( drawableProvider.getDrawable(R.drawable.ic_beta_pill), null ) - avatarRenderer.renderSpace(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index b6f1fb6a4e..eb02ed7c2d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -148,8 +148,8 @@ class SpacePreviewFragment @Inject constructor( // val roomPeekResult = preview.summary.roomPeekResult val spaceName = spacePreviewState.spaceInfo.invoke()?.name ?: spacePreviewState.name ?: "" val spaceAvatarUrl = spacePreviewState.spaceInfo.invoke()?.avatarUrl ?: spacePreviewState.avatarUrl - val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + val mxItem = MatrixItem.SpaceItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) + avatarRenderer.render(mxItem, views.spacePreviewToolbarAvatar) views.roomPreviewNoPreviewToolbarTitle.text = spaceName // } // is SpacePeekResult.SpacePeekError, diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt index 367a81fe5a..1856edb61f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt @@ -48,8 +48,8 @@ abstract class SubSpaceItem : VectorEpoxyModel() { super.bind(holder) holder.nameText.text = title - avatarRenderer.renderSpace( - MatrixItem.RoomItem(roomId, title, avatarUrl), + avatarRenderer.render( + MatrixItem.SpaceItem(roomId, title, avatarUrl), holder.avatarImageView ) holder.tabView.tabDepth = depth diff --git a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt index 0d28a4f912..86b7d0b0dc 100644 --- a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt @@ -39,8 +39,8 @@ class SharedPreferencesUiStateRepository @Inject constructor( override fun getDisplayMode(): RoomListDisplayMode { val result = when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE - VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS - VALUE_DISPLAY_MODE_ALL -> RoomListDisplayMode.ALL + VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS + VALUE_DISPLAY_MODE_ALL -> RoomListDisplayMode.ALL else -> if (vectorPreferences.labAddNotificationTab()) { RoomListDisplayMode.NOTIFICATIONS } else { @@ -101,6 +101,18 @@ class SharedPreferencesUiStateRepository @Inject constructor( return sharedPreferences.getBoolean("$KEY_SELECTED_METHOD@$sessionId", true) } + override fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set) { + sharedPreferences.edit { + putStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", servers) + } + } + + override fun getCustomRoomDirectoryHomeservers(sessionId: String): Set { + return sharedPreferences.getStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", null) + .orEmpty() + .toSet() + } + companion object { private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE" private const val VALUE_DISPLAY_MODE_CATCHUP = 0 @@ -111,5 +123,7 @@ class SharedPreferencesUiStateRepository @Inject constructor( private const val KEY_SELECTED_SPACE = "UI_STATE_SELECTED_SPACE" private const val KEY_SELECTED_GROUP = "UI_STATE_SELECTED_GROUP" private const val KEY_SELECTED_METHOD = "UI_STATE_SELECTED_METHOD" + + private const val KEY_CUSTOM_DIRECTORY_HOMESERVER = "KEY_CUSTOM_DIRECTORY_HOMESERVER" } } diff --git a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt index 935da83f5d..3c48f8972d 100644 --- a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt +++ b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt @@ -32,6 +32,7 @@ interface UiStateRepository { fun storeDisplayMode(displayMode: RoomListDisplayMode) + // TODO Handle SharedPreference per session in a better way, also to cleanup when login out fun storeSelectedSpace(spaceId: String?, sessionId: String) fun storeSelectedGroup(groupId: String?, sessionId: String) @@ -40,4 +41,7 @@ interface UiStateRepository { fun getSelectedSpace(sessionId: String): String? fun getSelectedGroup(sessionId: String): String? fun isGroupingMethodSpace(sessionId: String): Boolean + + fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set) + fun getCustomRoomDirectoryHomeservers(sessionId: String): Set } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index f9acfb3ce6..de88baa692 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -284,7 +284,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo ) ) launchWidgetAPIAction(widgetPostAPIMediator, eventData) { - session.updateAccountData( + session.userAccountDataService().updateAccountData( type = UserAccountDataTypes.TYPE_WIDGETS, content = addUserWidgetBody ) diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index 1c3ad7563c..00b388bfb8 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -35,7 +35,7 @@ import io.reactivex.functions.Function4 import io.reactivex.subjects.PublishSubject import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent +import org.matrix.android.sdk.api.session.accountdata.AccountDataEvent import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME @@ -98,8 +98,8 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS keysBackupState.value = session.cryptoService().keysBackupService().state - Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( - session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( + session.rx().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), session.rx().liveCrossSigningInfo(session.myUserId), keyBackupPublishSubject, session.rx().liveCrossSigningPrivateKeys(), diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt index 2f8d45043b..21c0c7481a 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt @@ -97,7 +97,7 @@ class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: ) } - session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) + session.rx().liveUserAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) .map { session.sharedSecretStorageService.isRecoverySetup() } diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml index 64ddd29319..5540eb91d3 100644 --- a/vector/src/main/res/layout/activity_call_transfer.xml +++ b/vector/src/main/res/layout/activity_call_transfer.xml @@ -52,7 +52,6 @@ android:layout_width="wrap_content" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:enabled="false" android:layout_height="wrap_content"/> + + + + + + app:layout_constraintTop_toBottomOf="@id/matrixToAccessText"> - + android:layout_height="wrap_content" + android:elevation="4dp"> - - + android:layout_height="match_parent" + android:theme="@style/Vector.Toolbar.Profile" + app:contentScrim="?riotx_background" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:scrimAnimationDuration="250" + app:scrimVisibleHeightTrigger="120dp" + app:titleEnabled="false" + app:toolbarId="@+id/toolbar"> - + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_room_directory.xml b/vector/src/main/res/layout/item_room_directory.xml index 391f52ad92..19a457ec37 100644 --- a/vector/src/main/res/layout/item_room_directory.xml +++ b/vector/src/main/res/layout/item_room_directory.xml @@ -1,5 +1,4 @@ - + tools:src="@drawable/network_matrix" /> + app:layout_goneMarginEnd="@dimen/layout_horizontal_margin" + tools:text="@string/directory_server_native_rooms" + tools:visibility="visible" /> - + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_room_directory_server.xml b/vector/src/main/res/layout/item_room_directory_server.xml new file mode 100644 index 0000000000..5705e1c623 --- /dev/null +++ b/vector/src/main/res/layout/item_room_directory_server.xml @@ -0,0 +1,67 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_vertical_margin.xml b/vector/src/main/res/layout/item_vertical_margin.xml new file mode 100644 index 0000000000..fac46e47ea --- /dev/null +++ b/vector/src/main/res/layout/item_vertical_margin.xml @@ -0,0 +1,5 @@ + + diff --git a/vector/src/main/res/menu/menu_directory_server_picker.xml b/vector/src/main/res/menu/menu_directory_server_picker.xml deleted file mode 100644 index c544c80f8c..0000000000 --- a/vector/src/main/res/menu/menu_directory_server_picker.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml index 6ff931aa43..ef0374edc2 100755 --- a/vector/src/main/res/values/config.xml +++ b/vector/src/main/res/values/config.xml @@ -23,6 +23,7 @@ matrix.org + gitter.im diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8649b14ca3..80120b51bf 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -537,7 +537,7 @@ Create Account Log in Sign out - Home Server URL + Homeserver URL Identity Server URL Search @@ -625,9 +625,9 @@ Use custom server options (advanced) Please check your email to continue registration Registration with email and phone number at once is not supported yet until the api exists. Only the phone number will be taken into account.\n\nYou may add your email to your profile in settings. - This Home Server would like to make sure you are not a robot + This homeserver would like to make sure you are not a robot Username in use - Home Server: + Homeserver: Identity Server: I have verified my email address To reset your password, enter the email address linked to your account: @@ -713,7 +713,7 @@ Use default ${app_name} ringtone for incoming calls Allow fallback call assist server - Will use "%s" as assist when your home server does not offer one (your IP address will be shared during a call) + Will use "%s" as assist when your homeserver does not offer one (your IP address will be shared during a call) Incoming call ringtone Select ringtone for calls: @@ -1314,7 +1314,7 @@ Submit Logged in as - Home Server + Homeserver Identity Server Allow integrations Integration Manager @@ -1584,9 +1584,14 @@ Select a room directory The server may be unavailable or overloaded Type a homeserver to list public rooms from - Homeserver URL + Server name All rooms on %s server All native %s rooms + Your server + Add a new server + Enter the name of a new server you want to explore. + "Can't find this server or its room list" + This server is already present in the list Type here… @@ -3005,11 +3010,11 @@ Sends the given message with confetti - Sends the given message with snow + Sends the given message with snowfall sends confetti 🎉 - sends snow ❄️ + sends snowfall ❄️ Unencrypted Encrypted by an unverified device @@ -3232,7 +3237,9 @@ Transfer An error occurred while transferring call Users - + Consulting with %1$s + Transfer to %1$s + Unknown person Re-Authentication Needed @@ -3262,6 +3269,7 @@ Do not notify View read receipts This room is public + This Space is public Dev Tools Explore Room State @@ -3293,6 +3301,8 @@ Delete unsent messages Are you sure you want to delete all unsent messages in this room? + Public space + Private space Add Space Your public space Your private space @@ -3388,4 +3398,5 @@ Some rooms may be hidden because they’re private and you need an invite.\nYou don’t have permission to add rooms. Some rooms may be hidden because they’re private and you need an invite. + Unnamed Room