diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml
index 9cd33143ad..8fe51eb8d5 100644
--- a/.github/workflows/post-pr.yml
+++ b/.github/workflows/post-pr.yml
@@ -325,5 +325,5 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
- text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
- html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
+ text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
+ html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}
{{icon conclusion}} {{name}} {{conclusion}} at {{completed_at}} [details]{{/if}}{{/with}}{{/each}}"
diff --git a/changelog.d/5658.feature b/changelog.d/5658.feature
new file mode 100644
index 0000000000..ba41a03207
--- /dev/null
+++ b/changelog.d/5658.feature
@@ -0,0 +1 @@
+Space explore screen changes: removed space card, added rooms filtering
diff --git a/changelog.d/5689.wip b/changelog.d/5689.wip
new file mode 100644
index 0000000000..ccea1ec541
--- /dev/null
+++ b/changelog.d/5689.wip
@@ -0,0 +1 @@
+[Live location sharing] Update message in timeline during the live
diff --git a/changelog.d/5724.sdk b/changelog.d/5724.sdk
new file mode 100644
index 0000000000..5a0a37fe31
--- /dev/null
+++ b/changelog.d/5724.sdk
@@ -0,0 +1 @@
+- Notifies other devices when a verification request sent from an Android device is accepted.`
diff --git a/changelog.d/5728.misc b/changelog.d/5728.misc
new file mode 100644
index 0000000000..6e463fa76f
--- /dev/null
+++ b/changelog.d/5728.misc
@@ -0,0 +1 @@
+leaving space experience changed to be aligned with iOS
diff --git a/changelog.d/6041.misc b/changelog.d/6041.misc
new file mode 100644
index 0000000000..50378ea3fd
--- /dev/null
+++ b/changelog.d/6041.misc
@@ -0,0 +1 @@
+Remove ShortcutBadger lib and usage (it was dead code)
diff --git a/changelog.d/6095.bugfix b/changelog.d/6095.bugfix
new file mode 100644
index 0000000000..11110bfa08
--- /dev/null
+++ b/changelog.d/6095.bugfix
@@ -0,0 +1 @@
+Correct .well-known/matrix/client handling for server_names which include ports.
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 76869fccf1..59cefe7e89 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -141,7 +141,6 @@ ext.groups = [
'jline',
'jp.wasabeef',
'junit',
- 'me.leolin',
'me.saket',
'net.bytebuddy',
'net.java',
diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt
index 2efb439ace..aeb5ae7914 100644
--- a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt
+++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt
@@ -16,6 +16,7 @@
package im.vector.lib.core.utils.flow
+import android.os.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
@@ -68,10 +69,10 @@ fun Flow.chunk(durationInMillis: Long): Flow> {
@ExperimentalCoroutinesApi
fun Flow.throttleFirst(windowDuration: Long): Flow = flow {
- var windowStartTime = System.currentTimeMillis()
+ var windowStartTime = SystemClock.elapsedRealtime()
var emitted = false
collect { value ->
- val currentTime = System.currentTimeMillis()
+ val currentTime = SystemClock.elapsedRealtime()
val delta = currentTime - windowStartTime
if (delta >= windowDuration) {
windowStartTime += delta / windowDuration * windowDuration
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
index fac7099b37..66dfcc5dc3 100644
--- a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
@@ -18,11 +18,11 @@ package org.billcarsonfr.jsonviewer
import android.content.ClipData
import android.content.ClipboardManager
-import android.content.Context
import android.view.ContextMenu
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.core.content.getSystemService
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyHolder
import com.airbnb.epoxy.EpoxyModelClass
@@ -77,8 +77,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder() {
) {
if (copyValue != null) {
val menuItem = menu?.add(R.string.copy_value)
- val clipService =
- v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
+ val clipService = v?.context?.getSystemService()
menuItem?.setOnMenuItemClickListener {
clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
true
diff --git a/library/ui-styles/src/main/res/values/styles_location.xml b/library/ui-styles/src/main/res/values/styles_location.xml
index 5563d28342..7571265241 100644
--- a/library/ui-styles/src/main/res/values/styles_location.xml
+++ b/library/ui-styles/src/main/res/values/styles_location.xml
@@ -2,10 +2,20 @@
+
+
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index df3b2ffe27..ceebc3cd01 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -27,11 +27,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
@@ -252,4 +254,48 @@ class VerificationTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
+
+ @Test
+ fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
+ val defaultSessionParams = SessionTestParams(true)
+ val testHelper = CommonTestHelper(context())
+
+ val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
+ val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
+ val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
+
+ val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
+
+ val serviceOfVerified = aliceSessionToVerify.cryptoService().verificationService()
+ val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService()
+ val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService()
+
+ serviceOfVerifier.addListener(object : VerificationService.Listener {
+ override fun verificationRequestCreated(pr: PendingVerificationRequest) {
+ // Accept verification request
+ serviceOfVerifier.readyPendingVerification(
+ verificationMethods,
+ pr.otherUserId,
+ pr.transactionId!!,
+ )
+ }
+ })
+
+ serviceOfVerified.requestKeyVerification(
+ methods = verificationMethods,
+ otherUserId = aliceSessionToVerify.myUserId,
+ otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
+ )
+
+ testHelper.waitWithLatch { latch ->
+ testHelper.retryPeriodicallyWithLatch(latch) {
+ val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
+ requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
+ }
+ }
+
+ testHelper.signOutAndClose(aliceSessionToVerify)
+ testHelper.signOutAndClose(aliceSessionThatVerifies)
+ testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent)
+ }
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
index d5b4a07fc0..e407c1b42d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
@@ -140,9 +140,24 @@ class TimelineForwardPaginationTest : InstrumentedTest {
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
- // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
- // 6 + 1 + 50
- assertEquals(57, snapshot.size)
+
+ // We explicitly test all the types we expect here, as we expect 51 messages and "some" state events
+ // But state events can change over time. So this acts as a kinda documentation of what we expect and
+ // provides a good error message if it doesn't match
+
+ val snapshotTypes = mutableMapOf()
+ snapshot.groupingBy { it -> it.root.type }.eachCountTo(snapshotTypes)
+ // Some state events on room creation
+ assertEquals("m.room.name", 1, snapshotTypes.remove("m.room.name"))
+ assertEquals("m.room.guest_access", 1, snapshotTypes.remove("m.room.guest_access"))
+ assertEquals("m.room.history_visibility", 1, snapshotTypes.remove("m.room.history_visibility"))
+ assertEquals("m.room.join_rules", 1, snapshotTypes.remove("m.room.join_rules"))
+ assertEquals("m.room.power_levels", 1, snapshotTypes.remove("m.room.power_levels"))
+ assertEquals("m.room.create", 1, snapshotTypes.remove("m.room.create"))
+ assertEquals("m.room.member", 1, snapshotTypes.remove("m.room.member"))
+ // 50 from pagination + 1 context
+ assertEquals("m.room.message", 51, snapshotTypes.remove("m.room.message"))
+ assertEquals("Additional events found in timeline", setOf(), snapshotTypes.keys)
}
// Alice paginates once again FORWARD for 50 events
@@ -152,8 +167,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
val snapshot = runBlocking {
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
}
- // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
- snapshot.size == 6 + numberOfMessagesToSend &&
+ // 7 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
+ snapshot.size == 7 + numberOfMessagesToSend &&
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
// The timeline is fully loaded
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
index 6e5fed8df9..1a36adec44 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
@@ -74,8 +74,12 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
Timber.w(" event ${it.root}")
}
- // Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
- snapshot.size == 8
+ // Ok, we have the 9 first messages of the initial sync (room creation and bob invite and join events)
+ // create
+ // join alice
+ // power_levels, join_rules, history_visibility, guest_access, name
+ // invite, join bob
+ snapshot.size == 9
}
bobTimeline.addListener(eventsListener)
@@ -192,7 +196,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
Timber.w(" event ${it.root}")
}
- snapshot.size == 44 // 8 + 1 + 35
+ snapshot.size == 45 // 9 + 1 + 35
}
bobTimeline.addListener(eventsListener)
@@ -220,8 +224,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
// Bob can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
- // 8 for room creation item 60 message from Alice
- snapshot.size == 68 && // 8 + 60
+ // 9 for room creation item 60 message from Alice
+ snapshot.size == 69 && // 9 + 60U
snapshot.checkSendOrder(secondMessage, 30, 0) &&
snapshot.checkSendOrder(firstMessage, 30, 30)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
index 867e066e60..82f39806c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
@@ -177,7 +177,7 @@ object MatrixPatterns {
* - "@alice:domain.org".getDomain() will return "domain.org"
* - "@bob:domain.org:3455".getDomain() will return "domain.org:3455"
*/
- fun String.getDomain(): String {
+ fun String.getServerName(): String {
if (BuildConfig.DEBUG && !isUserId(this)) {
// They are some invalid userId localpart in the wild, but the domain part should be there anyway
Timber.w("Not a valid user ID: $this")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt
index 5a025f37e1..e4716d7794 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt
@@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) {
MismatchedKeys("m.key_mismatch", "Key mismatch"),
UserError("m.user_error", "User error"),
MismatchedUser("m.user_mismatch", "User mismatch"),
- QrCodeInvalid("m.qr_code.invalid", "Invalid QR code")
+ QrCodeInvalid("m.qr_code.invalid", "Invalid QR code"),
+ AcceptedByAnotherDevice("m.accepted", "Verification request accepted by another device")
}
fun safeValueOf(code: String?): CancelCode {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 16bdbd3432..7124d8a1a3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
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.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@@ -375,11 +376,11 @@ fun Event.getRelationContent(): RelationDefaultContent? {
content.toModel()?.relatesTo
} else {
content.toModel()?.relatesTo ?: run {
- // Special case to handle stickers, while there is only a local msgtype for stickers
- if (getClearType() == EventType.STICKER) {
- getClearContent().toModel()?.relatesTo
- } else {
- null
+ // Special cases when there is only a local msgtype for some event types
+ when (getClearType()) {
+ EventType.STICKER -> getClearContent().toModel()?.relatesTo
+ in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel()?.relatesTo
+ else -> null
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index b87bc25435..d05fdb951f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@@ -140,6 +141,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
EventType.STICKER -> root.getClearContent().toModel()
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+ in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
}
}
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 f1cfe3fee5..02dfce04b5 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
@@ -20,7 +20,7 @@ import android.net.Uri
import dagger.Lazy
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixPatterns
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -381,7 +381,7 @@ internal class DefaultAuthenticationService @Inject constructor(
return getWellknownTask.execute(
GetWellknownTask.Params(
- domain = matrixId.getDomain(),
+ domain = matrixId.getServerName().substringBeforeLast(":"),
homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults()
)
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index 6da674d6e4..af48283767 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -939,9 +939,25 @@ internal class DefaultVerificationService @Inject constructor(
updatePendingRequest(
existingRequest.copy(
- readyInfo = readyReq
+ readyInfo = readyReq
)
)
+
+ notifyOthersOfAcceptance(readyReq.transactionId, readyReq.fromDevice)
+ }
+
+ /**
+ * Gets a list of device ids excluding the current one.
+ */
+ private fun getMyOtherDeviceIds(): List = cryptoStore.getUserDevices(userId)?.keys?.filter { it != deviceId }.orEmpty()
+
+ /**
+ * Notifies other devices that the current verification transaction is being handled by [acceptedByDeviceId].
+ */
+ private fun notifyOthersOfAcceptance(transactionId: String, acceptedByDeviceId: String) {
+ val deviceIds = getMyOtherDeviceIds().filter { it != acceptedByDeviceId }
+ val transport = verificationTransportToDeviceFactory.createTransport(null)
+ transport.cancelTransaction(transactionId, userId, deviceIds, CancelCode.AcceptedByAnotherDevice)
}
private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt
index c12aea9d52..8538e5a5af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt
@@ -49,6 +49,11 @@ internal interface VerificationTransport {
otherUserDeviceId: String?,
code: CancelCode)
+ fun cancelTransaction(transactionId: String,
+ otherUserId: String,
+ otherUserDeviceIds: List,
+ code: CancelCode)
+
fun done(transactionId: String,
onDone: (() -> Unit)?)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
index e32828af23..03df849d22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt
@@ -160,6 +160,9 @@ internal class VerificationTransportRoomMessage(
}
}
+ override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) =
+ cancelTransaction(transactionId, otherUserId, null, code)
+
override fun done(transactionId: String,
onDone: (() -> Unit)?) {
Timber.d("## SAS sending done for $transactionId")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
index bc24ef2966..974adf3888 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
@@ -193,6 +193,27 @@ internal class VerificationTransportToDevice(
.executeBy(taskExecutor)
}
+ override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) {
+ Timber.d("## SAS canceling transaction $transactionId for reason $code")
+ val cancelMessage = KeyVerificationCancel.create(transactionId, code)
+ val contentMap = MXUsersDevicesMap()
+ val messages = otherUserDeviceIds.associateWith { cancelMessage }
+ contentMap.setObjects(otherUserId, messages)
+ sendToDeviceTask
+ .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
+ this.callback = object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
+ }
+
+ override fun onFailure(failure: Throwable) {
+ Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
+ }
+ }
+ }
+ .executeBy(taskExecutor)
+ }
+
override fun createAccept(tid: String,
keyAgreementProtocol: String,
hash: String,
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 7ceb89e892..b5f49d7f9c 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
@@ -87,8 +87,6 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
-import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DefaultLiveLocationAggregationProcessor
-import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
@@ -387,7 +385,4 @@ internal abstract class SessionModule {
@Binds
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
-
- @Binds
- abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index e9097e4d03..b1a518724c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orFalse
@@ -93,10 +93,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
}
}.getOrNull()
+ // Domain may include a port (eg, matrix.org:8080)
+ // Per https://spec.matrix.org/latest/client-server-api/#well-known-uri we should extract the hostname from the server name
+ // So we take everything before the last : as the domain for the well-known task.
+ // NB: This is not always the same endpoint as capabilities / mediaConfig uses.
val wellknownResult = runCatching {
getWellknownTask.execute(
GetWellknownTask.Params(
- domain = userId.getDomain(),
+ domain = userId.getServerName().substringBeforeLast(":"),
homeServerConnectionConfig = homeServerConnectionConfig
)
)
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 0f667c65df..edc45fe945 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
@@ -16,7 +16,7 @@
package org.matrix.android.sdk.internal.session.permalinks
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
@@ -55,9 +55,9 @@ internal class ViaParameterFinder @Inject constructor(
}
fun computeViaParams(userId: String, roomId: String, max: Int): List {
- val userHomeserver = userId.getDomain()
+ val userHomeserver = userId.getServerName()
return getUserIdsOfJoinedMembers(roomId)
- .map { it.getDomain() }
+ .map { it.getServerName() }
.groupBy { it }
.mapValues { it.value.size }
.toMutableMap()
@@ -92,7 +92,7 @@ internal class ViaParameterFinder @Inject constructor(
.orEmpty()
.toSet()
- return userThatCanInvite.map { it.getDomain() }
+ return userThatCanInvite.map { it.getServerName() }
.groupBy { it }
.mapValues { it.value.size }
.toMutableMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 16a63a9a96..af9c0071fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -193,9 +193,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
in EventType.BEACON_LOCATION_DATA -> {
- event.getClearContent().toModel(catchError = true)?.let {
- liveLocationAggregationProcessor.handleBeaconLocationData(realm, event, it, roomId, isLocalEcho)
- }
+ handleBeaconLocationData(event, realm, roomId, isLocalEcho)
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
@@ -260,6 +258,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
}
}
+ in EventType.BEACON_LOCATION_DATA -> {
+ handleBeaconLocationData(event, realm, roomId, isLocalEcho)
+ }
else -> Timber.v("UnHandled event ${event.eventId}")
}
} catch (t: Throwable) {
@@ -756,4 +757,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
verifSummary.sourceEvents.add(event.eventId)
}
}
+
+ private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
+ event.getClearContent().toModel(catchError = true)?.let {
+ liveLocationAggregationProcessor.handleBeaconLocationData(
+ realm,
+ event,
+ it,
+ roomId,
+ event.getRelationContent()?.eventId,
+ isLocalEcho
+ )
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt
deleted file mode 100644
index 997e31a109..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright 2022 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.aggregation.livelocation
-
-import io.realm.Realm
-import org.matrix.android.sdk.api.extensions.orTrue
-import org.matrix.android.sdk.api.session.events.model.Event
-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.model.message.MessageBeaconInfoContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
-import org.matrix.android.sdk.internal.database.mapper.ContentMapper
-import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
-import org.matrix.android.sdk.internal.database.query.getOrCreate
-import timber.log.Timber
-import javax.inject.Inject
-
-internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : LiveLocationAggregationProcessor {
-
- override fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
- if (event.senderId.isNullOrEmpty() || isLocalEcho) {
- return
- }
-
- val targetEventId = if (content.isLive.orTrue()) {
- event.eventId
- } else {
- // when live is set to false, we use the id of the event that should have been replaced
- event.unsignedData?.replacesState
- }
-
- if (targetEventId.isNullOrEmpty()) {
- Timber.w("no target event id found for the beacon content")
- return
- }
-
- val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
- realm = realm,
- roomId = roomId,
- eventId = targetEventId
- )
-
- Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}")
-
- aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
- aggregatedSummary.isActive = content.isLive
- }
-
- override fun handleBeaconLocationData(realm: Realm, event: Event, content: MessageBeaconLocationDataContent, roomId: String, isLocalEcho: Boolean) {
- if (event.senderId.isNullOrEmpty() || isLocalEcho) {
- return
- }
-
- val targetEventId = content.relatesTo?.eventId
-
- if (targetEventId.isNullOrEmpty()) {
- Timber.w("no target event id found for the live location content")
- return
- }
-
- val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
- realm = realm,
- roomId = roomId,
- eventId = targetEventId
- )
- val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
- val currentLocationTimestamp = ContentMapper
- .map(aggregatedSummary.lastLocationContent)
- .toModel()
- ?.getBestTimestampMillis()
- ?: 0
-
- if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
- Timber.d("updating last location of the summary of id=$targetEventId")
- aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent())
- }
- }
-
- private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
index c0be96f83d..76b7a4ec8e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -17,24 +17,83 @@
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
import io.realm.Realm
+import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.events.model.Event
+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.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import timber.log.Timber
+import javax.inject.Inject
-internal interface LiveLocationAggregationProcessor {
- fun handleBeaconInfo(
- realm: Realm,
- event: Event,
- content: MessageBeaconInfoContent,
- roomId: String,
- isLocalEcho: Boolean,
- )
+internal class LiveLocationAggregationProcessor @Inject constructor() {
+
+ fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
+ if (event.senderId.isNullOrEmpty() || isLocalEcho) {
+ return
+ }
+
+ val targetEventId = if (content.isLive.orTrue()) {
+ event.eventId
+ } else {
+ // when live is set to false, we use the id of the event that should have been replaced
+ event.unsignedData?.replacesState
+ }
+
+ if (targetEventId.isNullOrEmpty()) {
+ Timber.w("no target event id found for the beacon content")
+ return
+ }
+
+ val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
+ realm = realm,
+ roomId = roomId,
+ eventId = targetEventId
+ )
+
+ Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}")
+
+ aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
+ aggregatedSummary.isActive = content.isLive
+ }
fun handleBeaconLocationData(
realm: Realm,
event: Event,
content: MessageBeaconLocationDataContent,
roomId: String,
- isLocalEcho: Boolean,
- )
+ relatedEventId: String?,
+ isLocalEcho: Boolean
+ ) {
+ if (event.senderId.isNullOrEmpty() || isLocalEcho) {
+ return
+ }
+
+ if (relatedEventId.isNullOrEmpty()) {
+ Timber.w("no related event id found for the live location content")
+ return
+ }
+
+ val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
+ realm = realm,
+ roomId = roomId,
+ eventId = relatedEventId
+ )
+ val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
+ val currentLocationTimestamp = ContentMapper
+ .map(aggregatedSummary.lastLocationContent)
+ .toModel()
+ ?.getBestTimestampMillis()
+ ?: 0
+
+ if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
+ Timber.d("updating last location of the summary of id=$relatedEventId")
+ aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent())
+ }
+ }
+
+ private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
index 7c137a8102..fa19b4f9cf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
@@ -16,7 +16,7 @@
package org.matrix.android.sdk.internal.session.room.alias
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
import org.matrix.android.sdk.internal.di.UserId
@@ -65,6 +65,6 @@ internal class RoomAliasAvailabilityChecker @Inject constructor(
}
companion object {
- internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getDomain()
+ internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getServerName()
}
}
diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh
index e40d3635e8..910616176c 100755
--- a/tools/check/check_code_quality.sh
+++ b/tools/check/check_code_quality.sh
@@ -67,6 +67,9 @@ echo "Search for forbidden patterns in code..."
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt \
./matrix-sdk-android/src/main/java \
./matrix-sdk-android-flow/src/main/java \
+ ./library/core-utils/src/main/java \
+ ./library/jsonviewer/src/main/java \
+ ./library/ui-styles/src/main/java \
./vector/src/main/java \
./vector/src/debug/java \
./vector/src/release/java \
@@ -100,6 +103,7 @@ echo
echo "Search for forbidden patterns in resources..."
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt \
+ ./library/ui-styles/src/main/res/values \
./vector/src/main/res/color \
./vector/src/main/res/layout \
./vector/src/main/res/values \
diff --git a/vector/build.gradle b/vector/build.gradle
index 1c0a4718c4..99ced285cc 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -450,9 +450,6 @@ dependencies {
kapt libs.github.glideCompiler
implementation 'com.github.yalantis:ucrop:2.2.8'
- // Badge for compatibility
- implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
-
// Chat effects
implementation 'nl.dionsegijn:konfetti-xml:2.0.2'
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt
index 4d35e3c550..289c6e21b4 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt
@@ -104,11 +104,10 @@ class SpaceMenuRobot {
fun leaveSpace() {
clickOnSheet(R.id.leaveSpace)
- waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
- clickOn(R.id.leave_selected)
waitUntilActivityVisible {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
}
+ clickOn(R.id.spaceLeaveSelectAll)
clickOn(R.id.spaceLeaveButton)
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
}
diff --git a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
index b62520278a..a7d814052a 100755
--- a/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
+++ b/vector/src/gplay/java/im/vector/app/gplay/push/fcm/VectorFirebaseMessagingService.kt
@@ -1,15 +1,12 @@
/*
* Copyright 2019 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.
@@ -32,7 +29,6 @@ import im.vector.app.BuildConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.network.WifiDetector
import im.vector.app.core.pushers.PushersManager
-import im.vector.app.features.badge.BadgeProxy
import im.vector.app.features.notifications.NotifiableEventResolver
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.notifications.NotificationUtils
@@ -152,10 +148,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")
}
- // update the badge counter
- val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0
- BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
-
val session = activeSessionHolder.getSafeActiveSession()
if (session == null) {
diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html
index 0bead1f826..8f27776fbf 100755
--- a/vector/src/main/assets/open_source_licenses.html
+++ b/vector/src/main/assets/open_source_licenses.html
@@ -369,11 +369,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright 2012 Square, Inc.
-
- ShortcutBadger
-
- Copyright 2014 Leo Lin
-
diff-match-patch
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index d904a5a4de..8a4aaa4b26 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -55,6 +55,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ActivityEntryPoint
import im.vector.app.core.dialogs.DialogLocker
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
+import im.vector.app.core.error.fatalError
import im.vector.app.core.extensions.observeEvent
import im.vector.app.core.extensions.observeNotNull
import im.vector.app.core.extensions.registerStartForActivityResult
@@ -611,11 +612,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
}
}.show()
} else {
- if (vectorPreferences.failFast()) {
- error("No CoordinatorLayout to display this snackbar!")
- } else {
- Timber.w("No CoordinatorLayout to display this snackbar!")
- }
+ fatalError("No CoordinatorLayout to display this snackbar!", vectorPreferences.failFast())
}
}
diff --git a/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt b/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt
index 30cb1dcae4..6762bd68da 100644
--- a/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt
+++ b/vector/src/main/java/im/vector/app/core/resources/DateProvider.kt
@@ -19,27 +19,30 @@ package im.vector.app.core.resources
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
+import org.threeten.bp.ZoneOffset
object DateProvider {
- private val zoneId = ZoneId.systemDefault()
- private val zoneOffset by lazy {
- val now = currentLocalDateTime()
- zoneId.rules.getOffset(now)
- }
+ // recompute the zoneId each time we access it to handle change of timezones
+ private val defaultZoneId: ZoneId
+ get() = ZoneId.systemDefault()
+
+ // recompute the zoneOffset each time we access it to handle change of timezones
+ private val defaultZoneOffset: ZoneOffset
+ get() = defaultZoneId.rules.getOffset(currentLocalDateTime())
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0)
- return LocalDateTime.ofInstant(instant, zoneId)
+ return LocalDateTime.ofInstant(instant, defaultZoneId)
}
fun currentLocalDateTime(): LocalDateTime {
val instant = Instant.now()
- return LocalDateTime.ofInstant(instant, zoneId)
+ return LocalDateTime.ofInstant(instant, defaultZoneId)
}
fun toTimestamp(localDateTime: LocalDateTime): Long {
- return localDateTime.toInstant(zoneOffset).toEpochMilli()
+ return localDateTime.toInstant(defaultZoneOffset).toEpochMilli()
}
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
index 992a85679c..d2f8c4022b 100644
--- a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt
@@ -19,11 +19,15 @@ package im.vector.app.core.utils
import android.content.Context
import android.os.Build
import android.text.format.Formatter
+import im.vector.app.R
import org.threeten.bp.Duration
import java.util.TreeMap
object TextUtils {
+ private const val MINUTES_PER_HOUR = 60
+ private const val SECONDS_PER_MINUTE = 60
+
private val suffixes = TreeMap().also {
it[1000] = "k"
it[1000000] = "M"
@@ -71,13 +75,63 @@ object TextUtils {
}
fun formatDuration(duration: Duration): String {
- val hours = duration.seconds / 3600
- val minutes = (duration.seconds % 3600) / 60
- val seconds = duration.seconds % 60
+ val hours = getHours(duration)
+ val minutes = getMinutes(duration)
+ val seconds = getSeconds(duration)
return if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
}
+
+ fun formatDurationWithUnits(context: Context, duration: Duration): String {
+ val hours = getHours(duration)
+ val minutes = getMinutes(duration)
+ val seconds = getSeconds(duration)
+ val builder = StringBuilder()
+ when {
+ hours > 0 -> {
+ appendHours(context, builder, hours)
+ if (minutes > 0) {
+ builder.append(" ")
+ appendMinutes(context, builder, minutes)
+ }
+ if (seconds > 0) {
+ builder.append(" ")
+ appendSeconds(context, builder, seconds)
+ }
+ }
+ minutes > 0 -> {
+ appendMinutes(context, builder, minutes)
+ if (seconds > 0) {
+ builder.append(" ")
+ appendSeconds(context, builder, seconds)
+ }
+ }
+ else -> {
+ appendSeconds(context, builder, seconds)
+ }
+ }
+ return builder.toString()
+ }
+
+ private fun appendHours(context: Context, builder: StringBuilder, hours: Int) {
+ builder.append(hours)
+ builder.append(context.resources.getString(R.string.time_unit_hour_short))
+ }
+
+ private fun appendMinutes(context: Context, builder: StringBuilder, minutes: Int) {
+ builder.append(minutes)
+ builder.append(context.getString(R.string.time_unit_minute_short))
+ }
+
+ private fun appendSeconds(context: Context, builder: StringBuilder, seconds: Int) {
+ builder.append(seconds)
+ builder.append(context.getString(R.string.time_unit_second_short))
+ }
+
+ private fun getHours(duration: Duration): Int = duration.toHours().toInt()
+ private fun getMinutes(duration: Duration): Int = duration.toMinutes().toInt() % MINUTES_PER_HOUR
+ private fun getSeconds(duration: Duration): Int = (duration.seconds % SECONDS_PER_MINUTE).toInt()
}
diff --git a/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt
new file mode 100644
index 0000000000..c829313256
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022 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.utils
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import com.google.android.material.appbar.AppBarLayout
+
+/**
+ * [AppBarLayout.Behavior] subclass with a possibility to disable behavior.
+ * Useful for cases when in some view state we want prevent toolbar from collapsing/expanding by scroll events
+ */
+class ToggleableAppBarLayoutBehavior : AppBarLayout.Behavior {
+ constructor() : super()
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ var isEnabled = true
+
+ override fun onStartNestedScroll(parent: CoordinatorLayout,
+ child: AppBarLayout,
+ directTargetChild: View,
+ target: View,
+ nestedScrollAxes: Int,
+ type: Int): Boolean {
+ return isEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
+ }
+
+ override fun onNestedScroll(coordinatorLayout: CoordinatorLayout,
+ child: AppBarLayout,
+ target: View,
+ dxConsumed: Int,
+ dyConsumed: Int,
+ dxUnconsumed: Int,
+ dyUnconsumed: Int,
+ type: Int,
+ consumed: IntArray) {
+ if (!isEnabled) return
+ super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
+ }
+
+ override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout,
+ child: AppBarLayout,
+ target: View,
+ dx: Int,
+ dy: Int,
+ consumed: IntArray,
+ type: Int) {
+ if (!isEnabled) return
+ super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt b/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt
deleted file mode 100644
index c15ec8bdf4..0000000000
--- a/vector/src/main/java/im/vector/app/features/badge/BadgeProxy.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright 2019 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.
- */
-
-@file:Suppress("UNUSED_PARAMETER")
-
-package im.vector.app.features.badge
-
-import android.content.Context
-import android.os.Build
-import me.leolin.shortcutbadger.ShortcutBadger
-import org.matrix.android.sdk.api.session.Session
-
-/**
- * Manage application badge (displayed in the launcher).
- */
-object BadgeProxy {
-
- /**
- * Badge is now managed by notification channel, so no need to use compatibility library in recent versions.
- *
- * @return true if library ShortcutBadger can be used
- */
- private fun useShortcutBadger() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
-
- /**
- * Update the application badge value.
- *
- * @param context the context
- * @param badgeValue the new badge value
- */
- fun updateBadgeCount(context: Context, badgeValue: Int) {
- if (!useShortcutBadger()) {
- return
- }
-
- ShortcutBadger.applyCount(context, badgeValue)
- }
-
- /**
- * Refresh the badge count for specific configurations.
- * The refresh is only effective if the device is:
- * * offline * does not support FCM
- * * FCM registration failed
- *
Notifications rooms are parsed to track the notification count value.
- *
- * @param aSession session value
- * @param aContext App context
- */
- fun specificUpdateBadgeUnreadCount(aSession: Session?, aContext: Context?) {
- if (!useShortcutBadger()) {
- return
- }
-
- /* TODO
- val dataHandler: MXDataHandler
-
- // sanity check
- if (null == aContext || null == aSession) {
- Timber.w("## specificUpdateBadgeUnreadCount(): invalid input null values")
- } else {
- dataHandler = aSession.dataHandler
-
- if (dataHandler == null) {
- Timber.w("## specificUpdateBadgeUnreadCount(): invalid DataHandler instance")
- } else {
- if (aSession.isAlive) {
- var isRefreshRequired: Boolean
- val pushManager = Matrix.getInstance(aContext)!!.pushManager
-
- // update the badge count if the device is offline, FCM is not supported or FCM registration failed
- isRefreshRequired = !Matrix.getInstance(aContext)!!.isConnected
- isRefreshRequired = isRefreshRequired or (null != pushManager && (!pushManager.useFcm() || !pushManager.hasRegistrationToken()))
-
- if (isRefreshRequired) {
- updateBadgeCount(aContext, dataHandler)
- }
- }
- }
- }
- */
- }
-
- /**
- * Update the badge count value according to the rooms content.
- *
- * @param aContext App context
- * @param aDataHandler data handler instance
- */
- private fun updateBadgeCount(aSession: Session?, aContext: Context?) {
- if (!useShortcutBadger()) {
- return
- }
-
- /* TODO
- //sanity check
- if (null == aContext || null == aDataHandler) {
- Timber.w("## updateBadgeCount(): invalid input null values")
- } else if (null == aDataHandler.store) {
- Timber.w("## updateBadgeCount(): invalid store instance")
- } else {
- val roomCompleteList = ArrayList(aDataHandler.store.rooms)
- var unreadRoomsCount = 0
-
- for (room in roomCompleteList) {
- if (room.notificationCount > 0) {
- unreadRoomsCount++
- }
- }
-
- // update the badge counter
- Timber.v("## updateBadgeCount(): badge update count=$unreadRoomsCount")
- updateBadgeCount(aContext, unreadRoomsCount)
- }
- */
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt
deleted file mode 100644
index d233deffb8..0000000000
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2022 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.home.room.detail.timeline.factory
-
-import im.vector.app.core.epoxy.VectorEpoxyModel
-import im.vector.app.core.utils.DimensionConverter
-import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
-import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
-import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem
-import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_
-import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
-import javax.inject.Inject
-
-class LiveLocationMessageItemFactory @Inject constructor(
- private val dimensionConverter: DimensionConverter,
- private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
- private val avatarSizeProvider: AvatarSizeProvider,
-) {
-
- fun create(
- beaconInfoContent: MessageBeaconInfoContent,
- highlight: Boolean,
- attributes: AbsMessageItem.Attributes,
- ): VectorEpoxyModel<*>? {
- // TODO handle location received and stopped states
- return when {
- isLiveRunning(beaconInfoContent) -> buildStartLiveItem(highlight, attributes)
- else -> null
- }
- }
-
- private fun isLiveRunning(beaconInfoContent: MessageBeaconInfoContent): Boolean {
- // TODO when we will use aggregatedSummary, check if the live has timed out as well
- return beaconInfoContent.isLive.orFalse()
- }
-
- private fun buildStartLiveItem(
- highlight: Boolean,
- attributes: AbsMessageItem.Attributes,
- ): MessageLiveLocationStartItem {
- val width = timelineMediaSizeProvider.getMaxSize().first
- val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
-
- return MessageLiveLocationStartItem_()
- .attributes(attributes)
- .mapWidth(width)
- .mapHeight(height)
- .highlighted(highlight)
- .leftGuideline(avatarSizeProvider.leftGuideline)
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
new file mode 100644
index 0000000000..479a742369
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.factory
+
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.resources.DateProvider
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
+import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
+import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationInactiveItem
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationInactiveItem_
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem_
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem
+import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_
+import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
+import im.vector.app.features.location.UrlMapProvider
+import im.vector.app.features.location.toLocationData
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.threeten.bp.LocalDateTime
+import timber.log.Timber
+import javax.inject.Inject
+
+class LiveLocationShareMessageItemFactory @Inject constructor(
+ private val session: Session,
+ private val dimensionConverter: DimensionConverter,
+ private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
+ private val avatarSizeProvider: AvatarSizeProvider,
+ private val urlMapProvider: UrlMapProvider,
+ private val locationPinProvider: LocationPinProvider,
+ private val vectorDateFormatter: VectorDateFormatter,
+) {
+
+ fun create(
+ event: TimelineEvent,
+ highlight: Boolean,
+ attributes: AbsMessageItem.Attributes,
+ ): VectorEpoxyModel<*>? {
+ val liveLocationShareSummaryData = getLiveLocationShareSummaryData(event)
+ val item = when (val currentState = getViewState(liveLocationShareSummaryData)) {
+ LiveLocationShareViewState.Inactive -> buildInactiveItem(highlight, attributes)
+ LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes)
+ is LiveLocationShareViewState.Running -> buildRunningItem(highlight, attributes, currentState)
+ LiveLocationShareViewState.Unkwown -> null
+ }
+ item?.layout(attributes.informationData.messageLayout.layoutRes)
+
+ return item
+ }
+
+ private fun buildInactiveItem(
+ highlight: Boolean,
+ attributes: AbsMessageItem.Attributes,
+ ): MessageLiveLocationInactiveItem {
+ val width = timelineMediaSizeProvider.getMaxSize().first
+ val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
+
+ return MessageLiveLocationInactiveItem_()
+ .attributes(attributes)
+ .mapWidth(width)
+ .mapHeight(height)
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ }
+
+ private fun buildLoadingItem(
+ highlight: Boolean,
+ attributes: AbsMessageItem.Attributes,
+ ): MessageLiveLocationStartItem {
+ val width = timelineMediaSizeProvider.getMaxSize().first
+ val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
+
+ return MessageLiveLocationStartItem_()
+ .attributes(attributes)
+ .mapWidth(width)
+ .mapHeight(height)
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ }
+
+ private fun buildRunningItem(
+ highlight: Boolean,
+ attributes: AbsMessageItem.Attributes,
+ runningState: LiveLocationShareViewState.Running,
+ ): MessageLiveLocationItem {
+ // TODO only render location if enabled in preferences: to be handled in a next PR
+ val width = timelineMediaSizeProvider.getMaxSize().first
+ val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
+
+ val locationUrl = runningState.lastGeoUri.toLocationData()?.let {
+ urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
+ }
+
+ return MessageLiveLocationItem_()
+ .attributes(attributes)
+ .locationUrl(locationUrl)
+ .mapWidth(width)
+ .mapHeight(height)
+ .locationUserId(attributes.informationData.senderId)
+ .locationPinProvider(locationPinProvider)
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ .currentUserId(session.myUserId)
+ .endOfLiveDateTime(runningState.endOfLiveDateTime)
+ .vectorDateFormatter(vectorDateFormatter)
+ }
+
+ private fun getViewState(liveLocationShareSummaryData: LiveLocationShareSummaryData?): LiveLocationShareViewState {
+ return when {
+ liveLocationShareSummaryData?.isActive == null -> LiveLocationShareViewState.Unkwown
+ liveLocationShareSummaryData.isActive.not() || isLiveTimedOut(liveLocationShareSummaryData) -> LiveLocationShareViewState.Inactive
+ liveLocationShareSummaryData.isActive && liveLocationShareSummaryData.lastGeoUri.isNullOrEmpty() -> LiveLocationShareViewState.Loading
+ else ->
+ LiveLocationShareViewState.Running(
+ liveLocationShareSummaryData.lastGeoUri.orEmpty(),
+ getEndOfLiveDateTime(liveLocationShareSummaryData)
+ )
+ }.also { viewState -> Timber.d("computed viewState: $viewState") }
+ }
+
+ private fun isLiveTimedOut(liveLocationShareSummaryData: LiveLocationShareSummaryData): Boolean {
+ return getEndOfLiveDateTime(liveLocationShareSummaryData)
+ ?.let { endOfLive ->
+ // this will only cover users with different timezones but not users with manually time set
+ val now = LocalDateTime.now()
+ now.isAfter(endOfLive)
+ }
+ .orFalse()
+ }
+
+ private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
+ return liveLocationShareSummaryData.endOfLiveTimestampMillis?.let { DateProvider.toLocalDateTime(timestamp = it) }
+ }
+
+ private fun getLiveLocationShareSummaryData(event: TimelineEvent): LiveLocationShareSummaryData? {
+ return event.annotations?.liveLocationShareAggregatedSummary?.let { summary ->
+ LiveLocationShareSummaryData(
+ isActive = summary.isActive,
+ endOfLiveTimestampMillis = summary.endOfLiveTimestampMillis,
+ lastGeoUri = summary.lastLocationDataContent?.getBestLocationInfo()?.geoUri
+ )
+ }
+ }
+
+ private data class LiveLocationShareSummaryData(
+ val isActive: Boolean?,
+ val endOfLiveTimestampMillis: Long?,
+ val lastGeoUri: String?,
+ )
+
+ private sealed class LiveLocationShareViewState {
+ object Loading : LiveLocationShareViewState()
+ data class Running(val lastGeoUri: String, val endOfLiveDateTime: LocalDateTime?) : LiveLocationShareViewState()
+ object Inactive : LiveLocationShareViewState()
+ object Unkwown : LiveLocationShareViewState()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index b960e2c6a9..13f783cded 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -148,7 +148,7 @@ class MessageItemFactory @Inject constructor(
private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider,
- private val liveLocationMessageItemFactory: LiveLocationMessageItemFactory,
+ private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
) {
// TODO inject this properly?
@@ -216,7 +216,7 @@ class MessageItemFactory @Inject constructor(
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
}
- is MessageBeaconInfoContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes)
+ is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
return messageItem?.apply {
@@ -237,14 +237,14 @@ class MessageItemFactory @Inject constructor(
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
}
- val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
+ val locationUserId = if (locationContent.isSelfLocation()) informationData.senderId else null
return MessageLocationItem_()
.attributes(attributes)
.locationUrl(locationUrl)
.mapWidth(width)
.mapHeight(height)
- .userId(userId)
+ .locationUserId(locationUserId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index f4bcc1ba65..07ae9d66c3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -100,7 +100,7 @@ class TimelineItemFactory @Inject constructor(
// Message itemsX
EventType.STICKER,
in EventType.POLL_START,
- EventType.MESSAGE -> messageItemFactory.create(params)
+ EventType.MESSAGE -> messageItemFactory.create(params)
EventType.REDACTION,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
@@ -113,14 +113,15 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_NEGOTIATE,
EventType.REACTION,
in EventType.POLL_RESPONSE,
- in EventType.POLL_END -> noticeItemFactory.create(params)
+ in EventType.POLL_END,
+ in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
- EventType.CALL_ANSWER -> callItemFactory.create(params)
+ EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
- EventType.ENCRYPTED -> {
+ EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params)
@@ -129,11 +130,11 @@ class TimelineItemFactory @Inject constructor(
}
}
EventType.KEY_VERIFICATION_CANCEL,
- EventType.KEY_VERIFICATION_DONE -> {
+ EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(params)
}
// Unhandled event types
- else -> {
+ else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(params)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 7ad0cb27c6..8e06b3ee5d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -107,7 +107,8 @@ class NoticeEventFormatter @Inject constructor(
EventType.REDACTION,
EventType.STICKER,
in EventType.POLL_RESPONSE,
- in EventType.POLL_END -> formatDebug(timelineEvent.root)
+ in EventType.POLL_END,
+ in EventType.BEACON_LOCATION_DATA -> formatDebug(timelineEvent.root)
else -> {
Timber.v("Type $type not handled by this formatter")
null
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 1e95f067d2..7874f843e1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
import javax.inject.Inject
/**
- * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline.
- * TODO Update this comment
+ * This class is responsible of building extra information data associated to a given event.
*/
class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter,
@@ -119,7 +118,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
isFirstFromThisSender = isFirstFromThisSender,
isLastFromThisSender = isLastFromThisSender,
e2eDecoration = e2eDecoration,
- sendStateDecoration = sendStateDecoration
+ sendStateDecoration = sendStateDecoration,
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
index 45c711ff93..737b0dc85d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt
@@ -57,6 +57,7 @@ class MessageItemAttributesFactory @Inject constructor(
memberClickListener = {
callback?.onMemberNameClicked(informationData)
},
+ callback = callback,
reactionPillCallback = callback,
avatarCallback = callback,
threadCallback = callback,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 263f105b6b..b9d79d5818 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -178,6 +178,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem
override val itemLongClickListener: View.OnLongClickListener? = null,
override val itemClickListener: ClickListener? = null,
val memberClickListener: ClickListener? = null,
+ val callback: TimelineEventController.Callback? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null,
val threadCallback: TimelineEventController.ThreadCallback? = null,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt
new file mode 100644
index 0000000000..f7146c24e9
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.home.room.detail.timeline.item
+
+import android.graphics.drawable.Drawable
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.airbnb.epoxy.EpoxyAttribute
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+import im.vector.app.R
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
+import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
+import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
+
+abstract class AbsMessageLocationItem : AbsMessageItem() {
+
+ @EpoxyAttribute
+ var locationUrl: String? = null
+
+ @EpoxyAttribute
+ var locationUserId: String? = null
+
+ @EpoxyAttribute
+ var mapWidth: Int = 0
+
+ @EpoxyAttribute
+ var mapHeight: Int = 0
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var locationPinProvider: LocationPinProvider? = null
+
+ override fun bind(holder: H) {
+ super.bind(holder)
+ renderSendState(holder.view, null)
+ bindMap(holder)
+ }
+
+ private fun bindMap(holder: Holder) {
+ val location = locationUrl ?: return
+ val messageLayout = attributes.informationData.messageLayout
+ val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
+ messageLayout.cornersRadius.granularRoundedCorners()
+ } else {
+ val dimensionConverter = DimensionConverter(holder.view.resources)
+ RoundedCorners(dimensionConverter.dpToPx(8))
+ }
+ holder.staticMapImageView.updateLayoutParams {
+ width = mapWidth
+ height = mapHeight
+ }
+ GlideApp.with(holder.staticMapImageView)
+ .load(location)
+ .apply(RequestOptions.centerCropTransform())
+ .listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean): Boolean {
+ holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed)
+ holder.staticMapErrorTextView.isVisible = true
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean): Boolean {
+ locationPinProvider?.create(locationUserId) { pinDrawable ->
+ // we are not using Glide since it does not display it correctly when there is no user photo
+ holder.staticMapPinImageView.setImageDrawable(pinDrawable)
+ }
+ holder.staticMapErrorTextView.isVisible = false
+ return false
+ }
+ })
+ .transform(imageCornerTransformation)
+ .into(holder.staticMapImageView)
+ }
+
+ abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
+ val staticMapImageView by bind(R.id.staticMapImageView)
+ val staticMapPinImageView by bind(R.id.staticMapPinImageView)
+ val staticMapErrorTextView by bind(R.id.staticMapErrorTextView)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt
new file mode 100644
index 0000000000..c421efda12
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultLiveLocationShareStatusItem.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.item
+
+import android.content.res.Resources
+import android.graphics.drawable.ColorDrawable
+import android.widget.ImageView
+import androidx.core.view.updateLayoutParams
+import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import im.vector.app.R
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
+import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
+import im.vector.app.features.themes.ThemeUtils
+
+/**
+ * Default implementation of common methods for item representing the status of a live location share.
+ */
+class DefaultLiveLocationShareStatusItem : LiveLocationShareStatusItem {
+
+ override fun bindMap(
+ mapImageView: ImageView,
+ mapWidth: Int,
+ mapHeight: Int,
+ messageLayout: TimelineMessageLayout
+ ) {
+ val mapCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
+ messageLayout.cornersRadius.granularRoundedCorners()
+ } else {
+ RoundedCorners(getDefaultLayoutCornerRadiusInDp(mapImageView.resources))
+ }
+ mapImageView.updateLayoutParams {
+ width = mapWidth
+ height = mapHeight
+ }
+ GlideApp.with(mapImageView)
+ .load(R.drawable.bg_no_location_map)
+ .transform(mapCornerTransformation)
+ .into(mapImageView)
+ }
+
+ override fun bindBottomBanner(bannerImageView: ImageView, messageLayout: TimelineMessageLayout) {
+ val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
+ GranularRoundedCorners(
+ 0f,
+ 0f,
+ messageLayout.cornersRadius.bottomEndRadius,
+ messageLayout.cornersRadius.bottomStartRadius
+ )
+ } else {
+ val bottomCornerRadius = getDefaultLayoutCornerRadiusInDp(bannerImageView.resources).toFloat()
+ GranularRoundedCorners(0f, 0f, bottomCornerRadius, bottomCornerRadius)
+ }
+ GlideApp.with(bannerImageView)
+ .load(ColorDrawable(ThemeUtils.getColor(bannerImageView.context, android.R.attr.colorBackground)))
+ .transform(imageCornerTransformation)
+ .into(bannerImageView)
+ }
+
+ private fun getDefaultLayoutCornerRadiusInDp(resources: Resources): Int {
+ val dimensionConverter = DimensionConverter(resources)
+ return dimensionConverter.dpToPx(8)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt
new file mode 100644
index 0000000000..2f79f2fc9e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/LiveLocationShareStatusItem.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.item
+
+import android.widget.ImageView
+import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
+
+interface LiveLocationShareStatusItem {
+ fun bindMap(
+ mapImageView: ImageView,
+ mapWidth: Int,
+ mapHeight: Int,
+ messageLayout: TimelineMessageLayout
+ )
+
+ fun bindBottomBanner(bannerImageView: ImageView, messageLayout: TimelineMessageLayout)
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
index 9620077fd8..258424c7de 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -42,7 +42,7 @@ data class MessageInformationData(
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
val isFirstFromThisSender: Boolean = false,
- val isLastFromThisSender: Boolean = false
+ val isLastFromThisSender: Boolean = false,
) : Parcelable {
val matrixItem: MatrixItem
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt
new file mode 100644
index 0000000000..bb85316bf1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationInactiveItem.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.item
+
+import android.widget.ImageView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+
+@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
+abstract class MessageLiveLocationInactiveItem :
+ AbsMessageItem(),
+ LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() {
+
+ @EpoxyAttribute
+ var mapWidth: Int = 0
+
+ @EpoxyAttribute
+ var mapHeight: Int = 0
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ renderSendState(holder.view, null)
+ bindMap(holder.noLocationMapImageView, mapWidth, mapHeight, attributes.informationData.messageLayout)
+ bindBottomBanner(holder.bannerImageView, attributes.informationData.messageLayout)
+ }
+
+ override fun getViewStubId() = STUB_ID
+
+ class Holder : AbsMessageItem.Holder(STUB_ID) {
+ val bannerImageView by bind(R.id.locationLiveInactiveBanner)
+ val noLocationMapImageView by bind(R.id.locationLiveInactiveMap)
+ }
+
+ companion object {
+ private const val STUB_ID = R.id.messageContentLiveLocationInactiveStub
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt
new file mode 100644
index 0000000000..838fbd46de
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.item
+
+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.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.toTimestamp
+import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.home.room.detail.RoomDetailAction
+import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
+import im.vector.app.features.location.live.LocationLiveMessageBannerView
+import im.vector.app.features.location.live.LocationLiveMessageBannerViewState
+import org.threeten.bp.LocalDateTime
+
+@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
+abstract class MessageLiveLocationItem : AbsMessageLocationItem() {
+
+ @EpoxyAttribute
+ var currentUserId: String? = null
+
+ @EpoxyAttribute
+ var endOfLiveDateTime: LocalDateTime? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ lateinit var vectorDateFormatter: VectorDateFormatter
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ bindLocationLiveBanner(holder)
+ }
+
+ private fun bindLocationLiveBanner(holder: Holder) {
+ // TODO in a future PR add check on device id to confirm that is the one that sent the beacon
+ val isEmitter = currentUserId != null && currentUserId == locationUserId
+ val messageLayout = attributes.informationData.messageLayout
+ val viewState = buildViewState(holder, messageLayout, isEmitter)
+ holder.locationLiveMessageBanner.isVisible = true
+ holder.locationLiveMessageBanner.render(viewState)
+ holder.locationLiveMessageBanner.stopButton.setOnClickListener {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.StopLiveLocationSharing)
+ }
+ }
+
+ private fun buildViewState(
+ holder: Holder,
+ messageLayout: TimelineMessageLayout,
+ isEmitter: Boolean
+ ): LocationLiveMessageBannerViewState {
+ return when {
+ messageLayout is TimelineMessageLayout.Bubble && isEmitter ->
+ LocationLiveMessageBannerViewState.Emitter(
+ remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
+ bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
+ bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
+ isStopButtonCenteredVertically = false
+ )
+ messageLayout is TimelineMessageLayout.Bubble ->
+ LocationLiveMessageBannerViewState.Watcher(
+ bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
+ bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
+ formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
+ )
+ isEmitter -> {
+ val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
+ LocationLiveMessageBannerViewState.Emitter(
+ remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
+ bottomStartCornerRadiusInDp = cornerRadius,
+ bottomEndCornerRadiusInDp = cornerRadius,
+ isStopButtonCenteredVertically = true
+ )
+ }
+ else -> {
+ val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
+ LocationLiveMessageBannerViewState.Watcher(
+ bottomStartCornerRadiusInDp = cornerRadius,
+ bottomEndCornerRadiusInDp = cornerRadius,
+ formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
+ )
+ }
+ }
+ }
+
+ private fun getBannerCornerRadiusForDefaultLayout(holder: Holder): Float {
+ val dimensionConverter = DimensionConverter(holder.view.resources)
+ return dimensionConverter.dpToPx(8).toFloat()
+ }
+
+ private fun getFormattedLocalTimeEndOfLive() =
+ endOfLiveDateTime?.toTimestamp()?.let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) }.orEmpty()
+
+ private fun getRemainingTimeOfLiveInMillis() =
+ (endOfLiveDateTime?.toTimestamp() ?: 0) - LocalDateTime.now().toTimestamp()
+
+ override fun getViewStubId() = STUB_ID
+
+ class Holder : AbsMessageLocationItem.Holder(STUB_ID) {
+ val locationLiveMessageBanner by bind(R.id.locationLiveMessageBanner)
+ }
+
+ companion object {
+ private const val STUB_ID = R.id.messageContentLiveLocationStub
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt
index 390db0ef50..001774b579 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationStartItem.kt
@@ -16,22 +16,15 @@
package im.vector.app.features.home.room.detail.timeline.item
-import android.graphics.drawable.ColorDrawable
import android.widget.ImageView
-import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
-import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R
-import im.vector.app.core.glide.GlideApp
-import im.vector.app.core.utils.DimensionConverter
-import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
-import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
-import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
-abstract class MessageLiveLocationStartItem : AbsMessageItem() {
+abstract class MessageLiveLocationStartItem :
+ AbsMessageItem(),
+ LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() {
@EpoxyAttribute
var mapWidth: Int = 0
@@ -42,44 +35,8 @@ abstract class MessageLiveLocationStartItem : AbsMessageItem() {
-
- @EpoxyAttribute
- var locationUrl: String? = null
-
- @EpoxyAttribute
- var userId: String? = null
-
- @EpoxyAttribute
- var mapWidth: Int = 0
-
- @EpoxyAttribute
- var mapHeight: Int = 0
-
- @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
- var locationPinProvider: LocationPinProvider? = null
-
- override fun bind(holder: Holder) {
- super.bind(holder)
- renderSendState(holder.view, null)
- val location = locationUrl ?: return
- val messageLayout = attributes.informationData.messageLayout
- val dimensionConverter = DimensionConverter(holder.view.resources)
- val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
- messageLayout.cornersRadius.granularRoundedCorners()
- } else {
- RoundedCorners(dimensionConverter.dpToPx(8))
- }
- holder.staticMapImageView.updateLayoutParams {
- width = mapWidth
- height = mapHeight
- }
- GlideApp.with(holder.staticMapImageView)
- .load(location)
- .apply(RequestOptions.centerCropTransform())
- .listener(object : RequestListener {
- override fun onLoadFailed(e: GlideException?,
- model: Any?,
- target: Target?,
- isFirstResource: Boolean): Boolean {
- holder.staticMapPinImageView.setImageResource(R.drawable.ic_location_pin_failed)
- holder.staticMapErrorTextView.isVisible = true
- return false
- }
-
- override fun onResourceReady(resource: Drawable?,
- model: Any?,
- target: Target?,
- dataSource: DataSource?,
- isFirstResource: Boolean): Boolean {
- locationPinProvider?.create(userId) { pinDrawable ->
- GlideApp.with(holder.staticMapPinImageView)
- .load(pinDrawable)
- .into(holder.staticMapPinImageView)
- }
- holder.staticMapErrorTextView.isVisible = false
- return false
- }
- })
- .transform(imageCornerTransformation)
- .into(holder.staticMapImageView)
- }
+abstract class MessageLocationItem : AbsMessageLocationItem() {
override fun getViewStubId() = STUB_ID
- class Holder : AbsMessageItem.Holder(STUB_ID) {
- val staticMapImageView by bind(R.id.staticMapImageView)
- val staticMapPinImageView by bind(R.id.staticMapPinImageView)
- val staticMapErrorTextView by bind(R.id.staticMapErrorTextView)
- }
+ class Holder : AbsMessageLocationItem.Holder(STUB_ID)
companion object {
private const val STUB_ID = R.id.messageContentLocationStub
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
index cb0b2384ec..a0d10a8a75 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt
@@ -66,6 +66,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_BEACON_INFO,
)
+
+ private val MSG_TYPES_WITH_LOCATION_DATA = setOf(
+ MessageType.MSGTYPE_LOCATION,
+ MessageType.MSGTYPE_BEACON_LOCATION_DATA
+ )
}
private val cornerRadius: Float by lazy {
@@ -145,9 +150,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
}
private fun MessageContent?.timestampInsideMessage(): Boolean {
- if (this == null) return false
- if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
- return this.msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
+ return when {
+ this == null -> false
+ msgType in MSG_TYPES_WITH_LOCATION_DATA -> vectorPreferences.labsRenderLocationsInTimeline()
+ else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
+ }
}
private fun MessageContent?.shouldAddMessageOverlay(): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt
new file mode 100644
index 0000000000..1ae017c98c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceDirectoryFilterNoResults.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 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.home.room.list
+
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+
+@EpoxyModelClass(layout = R.layout.item_space_directory_filter_no_results)
+abstract class SpaceDirectoryFilterNoResults : VectorEpoxyModel() {
+ class Holder : VectorEpoxyHolder()
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/Config.kt b/vector/src/main/java/im/vector/app/features/location/Config.kt
index 6f947290e2..c29e2e911a 100644
--- a/vector/src/main/java/im/vector/app/features/location/Config.kt
+++ b/vector/src/main/java/im/vector/app/features/location/Config.kt
@@ -22,5 +22,5 @@ const val DEFAULT_PIN_ID = "DEFAULT_PIN_ID"
const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0
const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0
-const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds
+const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 2 * 1_000L // every 2 seconds
const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt
index 061f338e72..b3466ff871 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt
@@ -29,7 +29,7 @@ data class LocationData(
) : Parcelable
/**
- * Creates location data from a LocationContent.
+ * Creates location data from a MessageLocationContent.
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
* @return location data or null if geo uri is not valid
*/
@@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? {
return parseGeo(getBestGeoUri())
}
+/**
+ * Creates location data from a geoUri String.
+ * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
+ * @return location data or null if geo uri is null or not valid
+ */
+fun String?.toLocationData(): LocationData? {
+ return this?.let { parseGeo(it) }
+}
+
@VisibleForTesting
fun parseGeo(geo: String): LocationData? {
val geoParts = geo
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt
index 362b82ccf5..8b9a1c75ae 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt
@@ -55,7 +55,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
private val binder = LocalBinder()
- private var roomArgsList = mutableListOf()
+ /**
+ * Keep track of a map between beacon event Id starting the live and RoomArgs.
+ */
+ private var roomArgsMap = mutableMapOf()
private var timers = mutableListOf()
override fun onCreate() {
@@ -73,8 +76,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
if (roomArgs != null) {
- roomArgsList.add(roomArgs)
-
// Show a sticky notification
val notification = notificationUtils.buildLiveLocationSharingNotification()
startForeground(roomArgs.roomId.hashCode(), notification)
@@ -87,7 +88,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
.getSafeActiveSession()
?.let { session ->
session.coroutineScope.launch(session.coroutineDispatchers.io) {
- sendLiveBeaconInfo(session, roomArgs)
+ sendStartingLiveBeaconInfo(session, roomArgs)
}
}
}
@@ -95,7 +96,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
return START_STICKY
}
- private suspend fun sendLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
+ private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
val beaconContent = MessageBeaconInfoContent(
timeout = roomArgs.durationMillis,
isLive = true,
@@ -103,7 +104,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
).toContent()
val stateKey = session.myUserId
- session
+ val beaconEventId = session
.getRoom(roomArgs.roomId)
?.stateService()
?.sendStateEvent(
@@ -111,6 +112,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
stateKey = stateKey,
body = beaconContent
)
+
+ beaconEventId
+ ?.takeUnless { it.isEmpty() }
+ ?.let {
+ roomArgsMap[it] = roomArgs
+ locationTracker.requestLastKnownLocation()
+ }
+ ?: run {
+ Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id")
+ }
}
private fun scheduleTimer(roomId: String, durationMillis: Long) {
@@ -134,9 +145,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
// Send a new beacon info state by setting live field as false
sendStoppedBeaconInfo(roomId)
- synchronized(roomArgsList) {
- roomArgsList.removeAll { it.roomId == roomId }
- if (roomArgsList.isEmpty()) {
+ synchronized(roomArgsMap) {
+ val beaconIds = roomArgsMap
+ .filter { it.value.roomId == roomId }
+ .map { it.key }
+ beaconIds.forEach { roomArgsMap.remove(it) }
+
+ if (roomArgsMap.isEmpty()) {
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
destroyMe()
}
@@ -156,16 +171,17 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
override fun onLocationUpdate(locationData: LocationData) {
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
- val session = activeSessionHolder.getSafeActiveSession()
// Emit location update to all rooms in which live location sharing is active
- session?.coroutineScope?.launch(session.coroutineDispatchers.io) {
- roomArgsList.toList().forEach { roomArg ->
- sendLiveLocation(roomArg.roomId, locationData)
- }
+ roomArgsMap.toMap().forEach { item ->
+ sendLiveLocation(item.value.roomId, item.key, locationData)
}
}
- private suspend fun sendLiveLocation(roomId: String, locationData: LocationData) {
+ private fun sendLiveLocation(
+ roomId: String,
+ beaconInfoEventId: String,
+ locationData: LocationData
+ ) {
val session = activeSessionHolder.getSafeActiveSession()
val room = session?.getRoom(roomId)
val userId = session?.myUserId
@@ -174,18 +190,12 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
return
}
- room
- .stateService()
- .getLiveLocationBeaconInfo(userId, true)
- ?.eventId
- ?.let {
- room.sendService().sendLiveLocation(
- beaconInfoEventId = it,
- latitude = locationData.latitude,
- longitude = locationData.longitude,
- uncertainty = locationData.uncertainty
- )
- }
+ room.sendService().sendLiveLocation(
+ beaconInfoEventId = beaconInfoEventId,
+ latitude = locationData.latitude,
+ longitude = locationData.longitude,
+ uncertainty = locationData.uncertainty
+ )
}
override fun onLocationProviderIsNotAvailable() {
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
index b7006370a6..4e56e7954c 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt
@@ -40,10 +40,12 @@ class LocationTracker @Inject constructor(
fun onLocationProviderIsNotAvailable()
}
- private var callbacks = mutableListOf()
+ private val callbacks = mutableListOf()
private var hasGpsProviderLiveLocation = false
+ private var lastLocation: LocationData? = null
+
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
fun start() {
Timber.d("## LocationTracker. start()")
@@ -92,6 +94,14 @@ class LocationTracker @Inject constructor(
callbacks.clear()
}
+ /**
+ * Request the last known location. It will be given async through Callback.
+ * Please ensure adding a callback to receive the value.
+ */
+ fun requestLastKnownLocation() {
+ lastLocation?.let { location -> callbacks.forEach { it.onLocationUpdate(location) } }
+ }
+
fun addCallback(callback: Callback) {
if (!callbacks.contains(callback)) {
callbacks.add(callback)
@@ -127,7 +137,9 @@ class LocationTracker @Inject constructor(
}
}
}
- callbacks.forEach { it.onLocationUpdate(location.toLocationData()) }
+ val locationData = location.toLocationData()
+ lastLocation = locationData
+ callbacks.forEach { it.onLocationUpdate(locationData) }
}
override fun onProviderDisabled(provider: String) {
diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt
new file mode 100644
index 0000000000..8cb552e3c4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerView.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2022 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.location.live
+
+import android.content.Context
+import android.graphics.drawable.ColorDrawable
+import android.os.CountDownTimer
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.view.isVisible
+import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
+import im.vector.app.R
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.core.utils.TextUtils
+import im.vector.app.databinding.ViewLocationLiveMessageBannerBinding
+import im.vector.app.features.themes.ThemeUtils
+import org.threeten.bp.Duration
+
+private const val REMAINING_TIME_COUNTER_INTERVAL_IN_MS = 1000L
+
+class LocationLiveMessageBannerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val binding = ViewLocationLiveMessageBannerBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+
+ val stopButton: Button
+ get() = binding.locationLiveMessageBannerStop
+
+ private val background: ImageView
+ get() = binding.locationLiveMessageBannerBackground
+
+ private val title: TextView
+ get() = binding.locationLiveMessageBannerTitle
+
+ private val subTitle: TextView
+ get() = binding.locationLiveMessageBannerSubTitle
+
+ private var countDownTimer: CountDownTimer? = null
+
+ fun render(viewState: LocationLiveMessageBannerViewState) {
+ when (viewState) {
+ is LocationLiveMessageBannerViewState.Emitter -> renderEmitter(viewState)
+ is LocationLiveMessageBannerViewState.Watcher -> renderWatcher(viewState)
+ }
+
+ GlideApp.with(context)
+ .load(ColorDrawable(ThemeUtils.getColor(context, android.R.attr.colorBackground)))
+ .transform(GranularRoundedCorners(0f, 0f, viewState.bottomEndCornerRadiusInDp, viewState.bottomStartCornerRadiusInDp))
+ .into(background)
+ }
+
+ private fun renderEmitter(viewState: LocationLiveMessageBannerViewState.Emitter) {
+ stopButton.isVisible = true
+ title.text = context.getString(R.string.location_share_live_enabled)
+
+ countDownTimer?.cancel()
+ viewState.remainingTimeInMillis
+ .takeIf { it >= 0 }
+ ?.let {
+ countDownTimer = object : CountDownTimer(it, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) {
+ override fun onTick(millisUntilFinished: Long) {
+ val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L))
+ subTitle.text = context.getString(
+ R.string.location_share_live_remaining_time,
+ TextUtils.formatDurationWithUnits(context, duration)
+ )
+ }
+
+ override fun onFinish() {
+ subTitle.text = context.getString(
+ R.string.location_share_live_remaining_time,
+ TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L))
+ )
+ }
+ }
+ countDownTimer?.start()
+ }
+
+ val rootLayout: ConstraintLayout? = (binding.root as? ConstraintLayout)
+ rootLayout?.let { parentLayout ->
+ val constraintSet = ConstraintSet()
+ constraintSet.clone(rootLayout)
+
+ if (viewState.isStopButtonCenteredVertically) {
+ constraintSet.connect(
+ R.id.locationLiveMessageBannerStop,
+ ConstraintSet.BOTTOM,
+ R.id.locationLiveMessageBannerBackground,
+ ConstraintSet.BOTTOM,
+ 0
+ )
+ } else {
+ constraintSet.clear(R.id.locationLiveMessageBannerStop, ConstraintSet.BOTTOM)
+ }
+
+ constraintSet.applyTo(parentLayout)
+ }
+ }
+
+ private fun renderWatcher(viewState: LocationLiveMessageBannerViewState.Watcher) {
+ stopButton.isVisible = false
+ title.text = context.getString(R.string.location_share_live_view)
+ subTitle.text = context.getString(R.string.location_share_live_until, viewState.formattedLocalTimeOfEndOfLive)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt
new file mode 100644
index 0000000000..976085386b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/live/LocationLiveMessageBannerViewState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.location.live
+
+sealed class LocationLiveMessageBannerViewState(
+ open val bottomStartCornerRadiusInDp: Float,
+ open val bottomEndCornerRadiusInDp: Float,
+) {
+
+ data class Emitter(
+ override val bottomStartCornerRadiusInDp: Float,
+ override val bottomEndCornerRadiusInDp: Float,
+ val remainingTimeInMillis: Long,
+ val isStopButtonCenteredVertically: Boolean
+ ) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp)
+
+ data class Watcher(
+ override val bottomStartCornerRadiusInDp: Float,
+ override val bottomEndCornerRadiusInDp: Float,
+ val formattedLocalTimeOfEndOfLive: String,
+ ) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp)
+}
diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
index c9cb4612a8..1b08d2a86f 100644
--- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt
@@ -36,7 +36,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ensureTrailingSlash
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -607,7 +607,7 @@ class LoginViewModel @AssistedInject constructor(
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
)
?: HomeServerConnectionConfig(
- homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
+ homeServerUri = Uri.parse("https://${action.username.getServerName()}"),
homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
)
diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
index e72e3a1790..6b9d255937 100644
--- a/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
+++ b/vector/src/main/java/im/vector/app/features/login2/LoginViewModel2.kt
@@ -38,7 +38,7 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -640,7 +640,7 @@ class LoginViewModel2 @AssistedInject constructor(
}
viewEvent?.let { _viewEvents.post(it) }
- val urlFromUser = action.username.getDomain()
+ val urlFromUser = action.username.getServerName()
setState {
copy(
isLoading = false,
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 7cc42ec57f..0f921ab80a 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
@@ -317,6 +317,7 @@ class DefaultNavigator @Inject constructor(
if (context is AppCompatActivity) {
if (context !is MatrixToBottomSheet.InteractionListener) {
fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast())
+ return
}
// TODO check if there is already one??
MatrixToBottomSheet.withLink(link, origin)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
index 171d8f7bb5..3014b199b4 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/DirectLoginUseCase.kt
@@ -20,7 +20,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.andThen
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@@ -75,7 +75,7 @@ class DirectLoginUseCase @Inject constructor(
)
private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
- homeServerUri = uriFactory.parse("https://${action.username.getDomain()}"),
+ homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"),
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
)
diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt
index 4dd5a68673..91269cb114 100644
--- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt
+++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt
@@ -16,14 +16,14 @@
package im.vector.app.features.raw.wellknown
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
suspend fun RawService.getElementWellknown(sessionParams: SessionParams): ElementWellKnown? {
// By default we use the domain of the userId to retrieve the .well-known data
- val domain = sessionParams.userId.getDomain()
+ val domain = sessionParams.userId.getServerName()
return tryOrNull { getWellknown(domain) }
?.let { ElementWellKnownMapper.from(it) }
}
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
index 1994de396f..f1306d9851 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
@@ -35,7 +35,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
@@ -98,7 +98,7 @@ class CreateRoomViewModel @AssistedInject constructor(
private fun initHomeServerName() {
setState {
copy(
- homeServerName = session.myUserId.getDomain()
+ homeServerName = session.myUserId.getServerName()
)
}
}
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 90283de77c..a168ea749c 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
@@ -20,7 +20,7 @@ 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.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import javax.inject.Inject
@@ -37,7 +37,7 @@ class RoomDirectoryListCreator @Inject constructor(
val protocols = ArrayList()
// Add user homeserver name
- val userHsName = session.myUserId.getDomain()
+ val userHsName = session.myUserId.getServerName()
// Add default protocol
protocols.add(
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
index 2641eb4184..a66ef9a659 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt
@@ -31,7 +31,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -96,7 +96,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo
private fun initHomeServerName() {
setState {
copy(
- homeServerName = session.myUserId.getDomain()
+ homeServerName = session.myUserId.getServerName()
)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt
deleted file mode 100644
index a292b64ddd..0000000000
--- a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * 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.spaces
-
-import android.app.Activity
-import android.graphics.Typeface
-import android.os.Bundle
-import android.os.Parcelable
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.text.toSpannable
-import androidx.core.view.isInvisible
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import com.airbnb.mvrx.Fail
-import com.airbnb.mvrx.Loading
-import com.airbnb.mvrx.args
-import com.airbnb.mvrx.parentFragmentViewModel
-import com.airbnb.mvrx.withState
-import dagger.hilt.android.AndroidEntryPoint
-import im.vector.app.R
-import im.vector.app.core.error.ErrorFormatter
-import im.vector.app.core.extensions.registerStartForActivityResult
-import im.vector.app.core.extensions.setTextOrHide
-import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.utils.styleMatchingText
-import im.vector.app.databinding.BottomSheetLeaveSpaceBinding
-import im.vector.app.features.displayname.getBestName
-import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.parcelize.Parcelize
-import me.gujun.android.span.span
-import org.matrix.android.sdk.api.util.toMatrixItem
-import reactivecircus.flowbinding.android.widget.checkedChanges
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() {
-
- val settingsViewModel: SpaceMenuViewModel by parentFragmentViewModel()
-
- override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetLeaveSpaceBinding {
- return BottomSheetLeaveSpaceBinding.inflate(inflater, container, false)
- }
-
- @Inject lateinit var colorProvider: ColorProvider
- @Inject lateinit var errorFormatter: ErrorFormatter
-
- @Parcelize
- data class Args(
- val spaceId: String
- ) : Parcelable
-
- override val showExpanded = true
-
- private val spaceArgs: SpaceBottomSheetSettingsArgs by args()
-
- private val cherryPickLeaveActivityResult = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- // nothing actually?
- } else {
- // move back to default
- settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll)
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- views.autoLeaveRadioGroup.checkedChanges()
- .onEach {
- when (it) {
- views.leaveAll.id -> {
- settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll)
- }
- views.leaveNone.id -> {
- settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveNone)
- }
- views.leaveSelected.id -> {
- settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveSelected)
- // launch dedicated activity
- cherryPickLeaveActivityResult.launch(
- SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId)
- )
- }
- }
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
-
- views.leaveButton.debouncedClicks {
- settingsViewModel.handle(SpaceLeaveViewAction.LeaveSpace)
- }
-
- views.cancelButton.debouncedClicks {
- dismiss()
- }
- }
-
- override fun invalidate() = withState(settingsViewModel) { state ->
- super.invalidate()
-
- val spaceSummary = state.spaceSummary ?: return@withState
- val bestName = spaceSummary.toMatrixItem().getBestName()
- val commonText = getString(R.string.space_leave_prompt_msg_with_name, bestName)
- .toSpannable().styleMatchingText(bestName, Typeface.BOLD)
-
- val warningMessage: CharSequence = if (spaceSummary.otherMemberIds.isEmpty()) {
- span {
- +commonText
- +"\n\n"
- span(getString(R.string.space_leave_prompt_msg_only_you)) {
- textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
- }
- }
- } else if (state.isLastAdmin) {
- span {
- +commonText
- +"\n\n"
- span(getString(R.string.space_leave_prompt_msg_as_admin)) {
- textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
- }
- }
- } else if (!spaceSummary.isPublic) {
- span {
- +commonText
- +"\n\n"
- span(getString(R.string.space_leave_prompt_msg_private)) {
- textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
- }
- }
- } else {
- commonText
- }
-
- views.bottomLeaveSpaceWarningText.setTextOrHide(warningMessage)
-
- views.inlineErrorText.setTextOrHide(null)
- if (state.leavingState is Loading) {
- views.leaveButton.isInvisible = true
- views.cancelButton.isInvisible = true
- views.leaveProgress.isVisible = true
- } else {
- views.leaveButton.isInvisible = false
- views.cancelButton.isInvisible = false
- views.leaveProgress.isVisible = false
- if (state.leavingState is Fail) {
- views.inlineErrorText.setTextOrHide(errorFormatter.toHumanReadable(state.leavingState.error))
- }
- }
-
- val hasChildren = (spaceSummary.spaceChildren?.size ?: 0) > 0
- if (hasChildren) {
- views.autoLeaveRadioGroup.isVisible = true
- when (state.leaveMode) {
- SpaceMenuState.LeaveMode.LEAVE_ALL -> {
- views.autoLeaveRadioGroup.check(views.leaveAll.id)
- }
- SpaceMenuState.LeaveMode.LEAVE_NONE -> {
- views.autoLeaveRadioGroup.check(views.leaveNone.id)
- }
- SpaceMenuState.LeaveMode.LEAVE_SELECTED -> {
- views.autoLeaveRadioGroup.check(views.leaveSelected.id)
- }
- }
- } else {
- views.autoLeaveRadioGroup.isVisible = false
- }
- }
-
- companion object {
-
- fun newInstance(spaceId: String): LeaveSpaceBottomSheet {
- return LeaveSpaceBottomSheet().apply {
- setArguments(SpaceBottomSheetSettingsArgs(spaceId))
- }
- }
- }
-}
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 78eab5b97f..94aa7e19b8 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
@@ -35,6 +35,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.navigation.Navigator
import im.vector.app.features.rageshake.BugReporter
import im.vector.app.features.roomprofile.RoomProfileActivity
+import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import kotlinx.parcelize.Parcelize
@@ -109,7 +110,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() {
interface InteractionListener {
+ fun onFilterQueryChanged(query: String?)
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
@@ -62,6 +65,7 @@ class SpaceDirectoryController @Inject constructor(
}
var listener: InteractionListener? = null
+ private val matchFilter = SpaceChildInfoMatchFilter()
override fun buildModels(data: SpaceDirectoryState?) {
val host = this
@@ -76,7 +80,7 @@ class SpaceDirectoryController @Inject constructor(
val failure = results.error
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
genericPillItem {
- id("HS no Support")
+ id("hs_no_support")
imageRes(R.drawable.error)
tintIcon(false)
text(
@@ -132,43 +136,52 @@ class SpaceDirectoryController @Inject constructor(
}
}
} else {
- flattenChildInfo.forEach { info ->
- val isSpace = info.roomType == RoomType.SPACE
- val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
- val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
- val error = (data?.changeMembershipStates?.get(info.childRoomId) as? ChangeMembershipState.FailedJoining)?.throwable
- // 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()
+ matchFilter.filter = data?.currentFilter ?: ""
+ val filteredChildInfo = flattenChildInfo.filter { matchFilter.test(it) }
- spaceChildInfoItem {
- id(info.childRoomId)
- matrixItem(matrixItem)
- avatarRenderer(host.avatarRenderer)
- topic(info.topic)
- suggested(info.suggested.orFalse())
- errorLabel(
- error?.let {
- host.stringProvider.getString(R.string.error_failed_to_join_room, host.errorFormatter.toHumanReadable(it))
+ if (filteredChildInfo.isEmpty()) {
+ spaceDirectoryFilterNoResults {
+ id("no_results")
+ }
+ } else {
+ filteredChildInfo.forEach { info ->
+ val isSpace = info.roomType == RoomType.SPACE
+ val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
+ val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
+ val error = (data?.changeMembershipStates?.get(info.childRoomId) as? ChangeMembershipState.FailedJoining)?.throwable
+ // 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(matrixItem)
+ avatarRenderer(host.avatarRenderer)
+ topic(info.topic)
+ suggested(info.suggested.orFalse())
+ errorLabel(
+ error?.let {
+ host.stringProvider.getString(R.string.error_failed_to_join_room, host.errorFormatter.toHumanReadable(it))
+ }
+ )
+ memberCount(info.activeMemberCount ?: 0)
+ loading(isLoading)
+ buttonLabel(
+ when {
+ error != null -> host.stringProvider.getString(R.string.global_retry)
+ isJoined -> host.stringProvider.getString(R.string.action_open)
+ else -> host.stringProvider.getString(R.string.action_join)
+ }
+ )
+ apply {
+ if (isSpace) {
+ itemClickListener { host.listener?.onSpaceChildClick(info) }
+ } else {
+ itemClickListener { host.listener?.onRoomClick(info) }
}
- )
- memberCount(info.activeMemberCount ?: 0)
- loading(isLoading)
- buttonLabel(
- when {
- error != null -> host.stringProvider.getString(R.string.global_retry)
- isJoined -> host.stringProvider.getString(R.string.action_open)
- else -> host.stringProvider.getString(R.string.action_join)
- }
- )
- apply {
- if (isSpace) {
- itemClickListener { host.listener?.onSpaceChildClick(info) }
- } else {
- itemClickListener { host.listener?.onRoomClick(info) }
}
+ buttonClickListener { host.listener?.onButtonClick(info) }
}
- buttonClickListener { host.listener?.onButtonClick(info) }
}
}
}
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 e59087778f..ed0bbdd911 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
@@ -23,6 +23,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.appcompat.widget.SearchView
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
@@ -44,7 +45,6 @@ import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentSpaceDirectoryBinding
import im.vector.app.features.analytics.plan.MobileScreen
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.SpaceAddRoomSpaceChooserBottomSheet
@@ -63,7 +63,6 @@ data class SpaceDirectoryArgs(
class SpaceDirectoryFragment @Inject constructor(
private val epoxyController: SpaceDirectoryController,
private val permalinkHandler: PermalinkHandler,
- private val spaceCardRenderer: SpaceCardRenderer,
private val colorProvider: ColorProvider
) : VectorBaseFragment(),
SpaceDirectoryController.InteractionListener,
@@ -123,9 +122,6 @@ class SpaceDirectoryFragment @Inject constructor(
}
}
- views.spaceCard.matrixToCardMainButton.isVisible = false
- views.spaceCard.matrixToCardSecondaryButton.isVisible = false
-
// Hide FAB when list is scrolling
views.spaceDirectoryList.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
@@ -167,18 +163,37 @@ class SpaceDirectoryFragment @Inject constructor(
// it's the root
toolbar?.setTitle(R.string.space_explore_activity_title)
} else {
- toolbar?.title = state.currentRootSummary?.name
+ val spaceName = state.currentRootSummary?.name
?: state.currentRootSummary?.canonicalAlias
- ?: getString(R.string.space_explore_activity_title)
+
+ if (spaceName != null) {
+ toolbar?.title = spaceName
+ toolbar?.subtitle = getString(R.string.space_explore_activity_title)
+ } else {
+ toolbar?.title = getString(R.string.space_explore_activity_title)
+ }
}
- spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard, showDescription = false)
views.addOrCreateChatRoomButton.isVisible = state.canAddRooms
}
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms
menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented
+
+ menu.findItem(R.id.spaceSearch)?.let { searchItem ->
+ val searchView = searchItem.actionView as SearchView
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ onFilterQueryChanged(newText)
+ return true
+ }
+ })
+ }
super.onPrepareOptionsMenu(menu)
}
@@ -198,6 +213,10 @@ class SpaceDirectoryFragment @Inject constructor(
return super.onOptionsItemSelected(item)
}
+ override fun onFilterQueryChanged(query: String?) {
+ viewModel.handle(SpaceDirectoryViewAction.FilterRooms(query))
+ }
+
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
}
diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt
index 2166a7e306..1d180eea4f 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewAction.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
sealed class SpaceDirectoryViewAction : VectorViewModelAction {
data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class JoinOrOpen(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
+ data class FilterRooms(val query: String?) : SpaceDirectoryViewAction()
data class ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
object CreateNewRoom : SpaceDirectoryViewAction()
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 2ddcb42e2a..7ae2feebcf 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
@@ -225,9 +225,16 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToCreateNewRoom(state.currentRootSummary?.roomId ?: initialState.spaceId))
}
}
+ is SpaceDirectoryViewAction.FilterRooms -> {
+ filter(action.query)
+ }
}
}
+ private fun filter(query: String?) {
+ setState { copy(currentFilter = query.orEmpty()) }
+ }
+
private fun handleBack() = withState { state ->
if (state.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt
index 68b313ec7f..a25476bff9 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewAction.kt
@@ -21,6 +21,9 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
+ data class SetFilteringEnabled(val isEnabled: Boolean) : SpaceLeaveAdvanceViewAction()
object DoLeave : SpaceLeaveAdvanceViewAction()
object ClearError : SpaceLeaveAdvanceViewAction()
+ object SelectAll : SpaceLeaveAdvanceViewAction()
+ object SelectNone : SpaceLeaveAdvanceViewAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt
index b8dcd3f7a2..fce5f4efa1 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvanceViewState.kt
@@ -28,8 +28,11 @@ data class SpaceLeaveAdvanceViewState(
val allChildren: Async> = Uninitialized,
val selectedRooms: List = emptyList(),
val currentFilter: String = "",
- val leaveState: Async = Uninitialized
+ val leaveState: Async = Uninitialized,
+ val isFilteringEnabled: Boolean = false,
+ val isLastAdmin: Boolean = false
) : MavericksState {
+
constructor(args: SpaceBottomSheetSettingsArgs) : this(
spaceId = args.spaceId
)
diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt
index 53c7481acb..308572a30f 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt
@@ -18,20 +18,23 @@ package im.vector.app.features.spaces.leave
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.lifecycleScope
+import androidx.appcompat.widget.SearchView
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
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.VectorBaseFragment
+import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior
import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
-import reactivecircus.flowbinding.appcompat.queryTextChanges
import javax.inject.Inject
class SpaceLeaveAdvancedFragment @Inject constructor(
@@ -44,11 +47,33 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
+ override fun getMenuRes() = R.menu.menu_space_leave
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- setupToolbar(views.toolbar)
- .allowBack()
+
controller.listener = this
+
+ withState(viewModel) { state ->
+ setupToolbar(views.toolbar)
+ .setSubtitle(state.spaceSummary?.name)
+ .allowBack()
+
+ state.spaceSummary?.let { summary ->
+ val warningMessage: CharSequence? = when {
+ summary.otherMemberIds.isEmpty() -> getString(R.string.space_leave_prompt_msg_only_you)
+ state.isLastAdmin -> getString(R.string.space_leave_prompt_msg_as_admin)
+ !summary.isPublic -> getString(R.string.space_leave_prompt_msg_private)
+ else -> null
+ }
+
+ views.spaceLeavePromptDescription.isVisible = warningMessage != null
+ views.spaceLeavePromptDescription.text = warningMessage
+ }
+
+ views.spaceLeavePromptTitle.text = getString(R.string.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
+ }
+
views.roomList.configureWith(controller)
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
@@ -56,12 +81,23 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
}
- views.publicRoomsFilter.queryTextChanges()
- .debounce(100)
- .onEach {
- viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString()))
- }
- .launchIn(viewLifecycleOwner.lifecycleScope)
+ views.spaceLeaveSelectGroup.setOnCheckedChangeListener { _, optionId ->
+ when (optionId) {
+ R.id.spaceLeaveSelectAll -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectAll)
+ R.id.spaceLeaveSelectNone -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectNone)
+ }
+ }
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ menu.findItem(R.id.menu_space_leave_search)?.let { searchItem ->
+ searchItem.bind(
+ onExpanded = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = true)) },
+ onCollapsed = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = false)) },
+ onTextChanged = { viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it)) }
+ )
+ }
+ super.onPrepareOptionsMenu(menu)
}
override fun onDestroyView() {
@@ -72,10 +108,63 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
+
+ if (state.isFilteringEnabled) {
+ views.appBarLayout.setExpanded(false)
+ }
+
+ updateAppBarBehaviorState(state)
+ updateRadioButtonsState(state)
+
controller.setData(state)
}
override fun onItemSelected(roomSummary: RoomSummary) {
viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId))
}
+
+ private fun updateAppBarBehaviorState(state: SpaceLeaveAdvanceViewState) {
+ val behavior = (views.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior as ToggleableAppBarLayoutBehavior
+ behavior.isEnabled = !state.isFilteringEnabled
+ }
+
+ private fun updateRadioButtonsState(state: SpaceLeaveAdvanceViewState) {
+ (state.allChildren as? Success)?.invoke()?.size?.let { allChildrenCount ->
+ when (state.selectedRooms.size) {
+ 0 -> views.spaceLeaveSelectNone.isChecked = true
+ allChildrenCount -> views.spaceLeaveSelectAll.isChecked = true
+ else -> views.spaceLeaveSelectSemi.isChecked = true
+ }
+ }
+ }
+
+ private fun MenuItem.bind(
+ onExpanded: () -> Unit,
+ onCollapsed: () -> Unit,
+ onTextChanged: (String) -> Unit) {
+ setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
+ override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
+ onExpanded()
+ return true
+ }
+
+ override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
+ onCollapsed()
+ return true
+ }
+ })
+
+ val searchView = actionView as SearchView
+
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ onTextChanged(newText ?: "")
+ return true
+ }
+ })
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt
index 3f5a27f696..2ab417ac55 100644
--- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt
@@ -36,9 +36,14 @@ import okhttp3.internal.toImmutableList
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.Session
+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.getRoom
-import org.matrix.android.sdk.api.session.getRoomSummary
+import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
+import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
@@ -50,52 +55,24 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
private val appStateHandler: AppStateHandler
) : VectorViewModel(initialState) {
- override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state ->
- when (action) {
- is SpaceLeaveAdvanceViewAction.ToggleSelection -> {
- val existing = state.selectedRooms.toMutableList()
- if (existing.contains(action.roomId)) {
- existing.remove(action.roomId)
- } else {
- existing.add(action.roomId)
- }
- setState {
- copy(
- selectedRooms = existing.toImmutableList()
- )
- }
- }
- is SpaceLeaveAdvanceViewAction.UpdateFilter -> {
- setState { copy(currentFilter = action.filter) }
- }
- SpaceLeaveAdvanceViewAction.DoLeave -> {
- setState { copy(leaveState = Loading()) }
- viewModelScope.launch {
- try {
- state.selectedRooms.forEach {
- try {
- session.roomService().leaveRoom(it)
- } catch (failure: Throwable) {
- // silently ignore?
- Timber.e(failure, "Fail to leave sub rooms/spaces")
- }
- }
+ init {
+ val space = session.getRoom(initialState.spaceId)
+ val spaceSummary = space?.roomSummary()
- session.spaceService().leaveSpace(initialState.spaceId)
- // We observe the membership and to dismiss when we have remote echo of leaving
- } catch (failure: Throwable) {
- setState { copy(leaveState = Fail(failure)) }
- }
- }
- }
- SpaceLeaveAdvanceViewAction.ClearError -> {
- setState { copy(leaveState = Uninitialized) }
+ val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
+ powerLevelsEvent?.content?.toModel()?.let { powerLevelsContent ->
+ val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
+ val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
+ val otherAdminCount = spaceSummary?.otherMemberIds
+ ?.map { powerLevelsHelper.getUserRole(it) }
+ ?.count { it is Role.Admin }
+ ?: 0
+ val isLastAdmin = isAdmin && otherAdminCount == 0
+ setState {
+ copy(isLastAdmin = isLastAdmin)
}
}
- }
- init {
- val spaceSummary = session.getRoomSummary(initialState.spaceId)
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)?.let { room ->
room.flow().liveRoomSummary()
@@ -127,6 +104,62 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
}
}
+ override fun handle(action: SpaceLeaveAdvanceViewAction) {
+ when (action) {
+ is SpaceLeaveAdvanceViewAction.UpdateFilter -> setState { copy(currentFilter = action.filter) }
+ SpaceLeaveAdvanceViewAction.ClearError -> setState { copy(leaveState = Uninitialized) }
+ SpaceLeaveAdvanceViewAction.SelectNone -> setState { copy(selectedRooms = emptyList()) }
+ is SpaceLeaveAdvanceViewAction.SetFilteringEnabled -> setState { copy(isFilteringEnabled = action.isEnabled) }
+ is SpaceLeaveAdvanceViewAction.ToggleSelection -> handleSelectionToggle(action)
+ SpaceLeaveAdvanceViewAction.DoLeave -> handleLeave()
+ SpaceLeaveAdvanceViewAction.SelectAll -> handleSelectAll()
+ }
+ }
+
+ private fun handleSelectAll() = withState { state ->
+ val filteredRooms = (state.allChildren as? Success)?.invoke()?.filter {
+ it.name.contains(state.currentFilter, true)
+ }
+ filteredRooms?.let {
+ setState { copy(selectedRooms = it.map { it.roomId }) }
+ }
+ }
+
+ private fun handleLeave() = withState { state ->
+ setState { copy(leaveState = Loading()) }
+ viewModelScope.launch {
+ try {
+ state.selectedRooms.forEach {
+ try {
+ session.roomService().leaveRoom(it)
+ } catch (failure: Throwable) {
+ // silently ignore?
+ Timber.e(failure, "Fail to leave sub rooms/spaces")
+ }
+ }
+
+ session.spaceService().leaveSpace(initialState.spaceId)
+ // We observe the membership and to dismiss when we have remote echo of leaving
+ } catch (failure: Throwable) {
+ setState { copy(leaveState = Fail(failure)) }
+ }
+ }
+ }
+
+ private fun handleSelectionToggle(action: SpaceLeaveAdvanceViewAction.ToggleSelection) = withState { state ->
+ val existing = state.selectedRooms.toMutableList()
+ if (existing.contains(action.roomId)) {
+ existing.remove(action.roomId)
+ } else {
+ existing.add(action.roomId)
+ }
+ setState {
+ copy(
+ selectedRooms = existing.toImmutableList(),
+ )
+ }
+ }
+
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory {
override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel
diff --git a/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp
index 23a45700f0..3241b5dc82 100644
Binary files a/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp and b/vector/src/main/res/drawable-hdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp
index a6130fba78..03f9ba5062 100644
Binary files a/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp and b/vector/src/main/res/drawable-mdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-night-hdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-night-hdpi/bg_no_location_map.webp
new file mode 100644
index 0000000000..76e0a75dd6
Binary files /dev/null and b/vector/src/main/res/drawable-night-hdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-night-mdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-night-mdpi/bg_no_location_map.webp
new file mode 100644
index 0000000000..79900cec1b
Binary files /dev/null and b/vector/src/main/res/drawable-night-mdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-night-xhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-night-xhdpi/bg_no_location_map.webp
new file mode 100644
index 0000000000..14f7e0e44c
Binary files /dev/null and b/vector/src/main/res/drawable-night-xhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-night-xxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-night-xxhdpi/bg_no_location_map.webp
new file mode 100644
index 0000000000..91cb7c8eb6
Binary files /dev/null and b/vector/src/main/res/drawable-night-xxhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-night-xxxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-night-xxxhdpi/bg_no_location_map.webp
new file mode 100644
index 0000000000..e4864a9eb2
Binary files /dev/null and b/vector/src/main/res/drawable-night-xxxhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp
index e908191371..513089b55b 100644
Binary files a/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp and b/vector/src/main/res/drawable-xhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp
index e062178367..50284965a7 100644
Binary files a/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp and b/vector/src/main/res/drawable-xxhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp b/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp
index 8b110d33fe..881af0055a 100644
Binary files a/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp and b/vector/src/main/res/drawable-xxxhdpi/bg_no_location_map.webp differ
diff --git a/vector/src/main/res/layout/bottom_sheet_leave_space.xml b/vector/src/main/res/layout/bottom_sheet_leave_space.xml
deleted file mode 100644
index 9e5a7c7ebf..0000000000
--- a/vector/src/main/res/layout/bottom_sheet_leave_space.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_space_directory.xml b/vector/src/main/res/layout/fragment_space_directory.xml
index bc77bb1474..8bc53b5243 100644
--- a/vector/src/main/res/layout/fragment_space_directory.xml
+++ b/vector/src/main/res/layout/fragment_space_directory.xml
@@ -11,35 +11,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="?attr/actionBarSize"
+ app:contentInsetStart="0dp">
+
@@ -57,7 +34,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
- android:layout_marginBottom="16dp "
+ android:layout_marginBottom="16dp"
android:contentDescription="@string/a11y_create_room"
android:scaleType="center"
android:src="@drawable/ic_fab_add"
diff --git a/vector/src/main/res/layout/fragment_space_leave_advanced.xml b/vector/src/main/res/layout/fragment_space_leave_advanced.xml
index 6216636458..67d9f044da 100644
--- a/vector/src/main/res/layout/fragment_space_leave_advanced.xml
+++ b/vector/src/main/res/layout/fragment_space_leave_advanced.xml
@@ -16,41 +16,107 @@
tools:listitem="@layout/item_room_to_add_in_space" />
+ android:layout_height="wrap_content"
+ app:layout_behavior="im.vector.app.core.utils.ToggleableAppBarLayoutBehavior">
-
-
+ android:layout_height="match_parent"
+ app:contentScrim="?android:colorBackground"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|enterAlways|snap"
+ app:scrimAnimationDuration="250"
+ app:scrimVisibleHeightTrigger="120dp"
+ app:titleEnabled="false"
+ app:toolbarId="@id/toolbar">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:padding="8dp"
+ app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
-
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_explore_space_child.xml b/vector/src/main/res/layout/item_explore_space_child.xml
index 8f984e1b92..eef664664d 100644
--- a/vector/src/main/res/layout/item_explore_space_child.xml
+++ b/vector/src/main/res/layout/item_explore_space_child.xml
@@ -136,8 +136,8 @@
android:layout_height="1dp"
android:background="?vctr_list_separator_system"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="@id/joinSuggestedRoomButton"
+ app:layout_constraintStart_toStartOf="@id/roomNameView"
app:layout_constraintTop_toBottomOf="@id/inlineErrorText" />
diff --git a/vector/src/main/res/layout/item_space_directory_filter_no_results.xml b/vector/src/main/res/layout/item_space_directory_filter_no_results.xml
new file mode 100644
index 0000000000..6a8ae89c9b
--- /dev/null
+++ b/vector/src/main/res/layout/item_space_directory_filter_no_results.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml b/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml
new file mode 100644
index 0000000000..d5a0cefb28
--- /dev/null
+++ b/vector/src/main/res/layout/item_timeline_event_live_location_inactive_stub.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml b/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml
index b81a6cc0e9..1726928721 100644
--- a/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_live_location_start_stub.xml
@@ -19,8 +19,8 @@
android:id="@+id/locationLiveStartBanner"
android:layout_width="0dp"
android:layout_height="48dp"
- android:alpha="0.85"
- android:src="?colorSurface"
+ android:alpha="0.75"
+ android:src="?android:colorBackground"
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartMap"
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
@@ -28,9 +28,10 @@
+ app:layout_constraintTop_toTopOf="@id/staticMapImageView"
+ app:layout_constraintVertical_bias="1.0" />
+
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
index 355d5fa7fe..0d45a48b9b 100644
--- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
+++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml
@@ -59,12 +59,24 @@
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_location_stub" />
+
+
+
+
diff --git a/vector/src/main/res/layout/view_location_live_message_banner.xml b/vector/src/main/res/layout/view_location_live_message_banner.xml
new file mode 100644
index 0000000000..35924541d1
--- /dev/null
+++ b/vector/src/main/res/layout/view_location_live_message_banner.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/menu/menu_space_directory.xml b/vector/src/main/res/menu/menu_space_directory.xml
index c95fb846af..395b271f1a 100644
--- a/vector/src/main/res/menu/menu_space_directory.xml
+++ b/vector/src/main/res/menu/menu_space_directory.xml
@@ -1,6 +1,16 @@
-