Merge branch 'develop' into dependency-cleanup
4
.github/workflows/post-pr.yml
vendored
@ -325,5 +325,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
|
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}}"
|
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 }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/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 }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
||||||
|
1
changelog.d/5658.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Space explore screen changes: removed space card, added rooms filtering
|
1
changelog.d/5689.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Live location sharing] Update message in timeline during the live
|
1
changelog.d/5724.sdk
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Notifies other devices when a verification request sent from an Android device is accepted.`
|
1
changelog.d/5728.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
leaving space experience changed to be aligned with iOS
|
1
changelog.d/6041.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Remove ShortcutBadger lib and usage (it was dead code)
|
1
changelog.d/6095.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Correct .well-known/matrix/client handling for server_names which include ports.
|
@ -141,7 +141,6 @@ ext.groups = [
|
|||||||
'jline',
|
'jline',
|
||||||
'jp.wasabeef',
|
'jp.wasabeef',
|
||||||
'junit',
|
'junit',
|
||||||
'me.leolin',
|
|
||||||
'me.saket',
|
'me.saket',
|
||||||
'net.bytebuddy',
|
'net.bytebuddy',
|
||||||
'net.java',
|
'net.java',
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.lib.core.utils.flow
|
package im.vector.lib.core.utils.flow
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@ -68,10 +69,10 @@ fun <T> Flow<T>.chunk(durationInMillis: Long): Flow<List<T>> {
|
|||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
|
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
|
||||||
var windowStartTime = System.currentTimeMillis()
|
var windowStartTime = SystemClock.elapsedRealtime()
|
||||||
var emitted = false
|
var emitted = false
|
||||||
collect { value ->
|
collect { value ->
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = SystemClock.elapsedRealtime()
|
||||||
val delta = currentTime - windowStartTime
|
val delta = currentTime - windowStartTime
|
||||||
if (delta >= windowDuration) {
|
if (delta >= windowDuration) {
|
||||||
windowStartTime += delta / windowDuration * windowDuration
|
windowStartTime += delta / windowDuration * windowDuration
|
||||||
|
@ -18,11 +18,11 @@ package org.billcarsonfr.jsonviewer
|
|||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
|
||||||
import android.view.ContextMenu
|
import android.view.ContextMenu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyHolder
|
import com.airbnb.epoxy.EpoxyHolder
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
@ -77,8 +77,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
|
|||||||
) {
|
) {
|
||||||
if (copyValue != null) {
|
if (copyValue != null) {
|
||||||
val menuItem = menu?.add(R.string.copy_value)
|
val menuItem = menu?.add(R.string.copy_value)
|
||||||
val clipService =
|
val clipService = v?.context?.getSystemService<ClipboardManager>()
|
||||||
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
||||||
menuItem?.setOnMenuItemClickListener {
|
menuItem?.setOnMenuItemClickListener {
|
||||||
clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
|
clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
|
||||||
true
|
true
|
||||||
|
@ -2,10 +2,20 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive">
|
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive">
|
||||||
<item name="android:background">?selectableItemBackground</item>
|
<item name="android:foreground">?selectableItemBackground</item>
|
||||||
|
<item name="android:background">@android:color/transparent</item>
|
||||||
<item name="android:textSize">12sp</item>
|
<item name="android:textSize">12sp</item>
|
||||||
<item name="android:padding">0dp</item>
|
<item name="android:padding">0dp</item>
|
||||||
<item name="android:gravity">center</item>
|
<item name="android:gravity">center</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Vector.Button.Text.LocationLive">
|
||||||
|
<item name="android:foreground">?selectableItemBackground</item>
|
||||||
|
<item name="android:background">@android:color/transparent</item>
|
||||||
|
<item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>
|
||||||
|
<item name="android:textColor">?colorError</item>
|
||||||
|
<item name="android:padding">0dp</item>
|
||||||
|
<item name="android:gravity">center</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -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.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
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.PendingVerificationRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
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.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
@ -252,4 +254,48 @@ class VerificationTest : InstrumentedTest {
|
|||||||
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,9 +140,24 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
|
||||||
assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
|
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
|
// We explicitly test all the types we expect here, as we expect 51 messages and "some" state events
|
||||||
assertEquals(57, snapshot.size)
|
// 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<String?, Int>()
|
||||||
|
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<String>(), snapshotTypes.keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alice paginates once again FORWARD for 50 events
|
// Alice paginates once again FORWARD for 50 events
|
||||||
@ -152,8 +167,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||||||
val snapshot = runBlocking {
|
val snapshot = runBlocking {
|
||||||
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
|
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
|
||||||
}
|
}
|
||||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
// 7 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||||
snapshot.size == 6 + numberOfMessagesToSend &&
|
snapshot.size == 7 + numberOfMessagesToSend &&
|
||||||
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||||
|
|
||||||
// The timeline is fully loaded
|
// The timeline is fully loaded
|
||||||
|
@ -74,8 +74,12 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||||||
Timber.w(" event ${it.root}")
|
Timber.w(" event ${it.root}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
|
// Ok, we have the 9 first messages of the initial sync (room creation and bob invite and join events)
|
||||||
snapshot.size == 8
|
// create
|
||||||
|
// join alice
|
||||||
|
// power_levels, join_rules, history_visibility, guest_access, name
|
||||||
|
// invite, join bob
|
||||||
|
snapshot.size == 9
|
||||||
}
|
}
|
||||||
|
|
||||||
bobTimeline.addListener(eventsListener)
|
bobTimeline.addListener(eventsListener)
|
||||||
@ -192,7 +196,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||||||
Timber.w(" event ${it.root}")
|
Timber.w(" event ${it.root}")
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.size == 44 // 8 + 1 + 35
|
snapshot.size == 45 // 9 + 1 + 35
|
||||||
}
|
}
|
||||||
|
|
||||||
bobTimeline.addListener(eventsListener)
|
bobTimeline.addListener(eventsListener)
|
||||||
@ -220,8 +224,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||||||
|
|
||||||
// Bob can see the first event of the room (so Back pagination has worked)
|
// Bob can see the first event of the room (so Back pagination has worked)
|
||||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||||
// 8 for room creation item 60 message from Alice
|
// 9 for room creation item 60 message from Alice
|
||||||
snapshot.size == 68 && // 8 + 60
|
snapshot.size == 69 && // 9 + 60U
|
||||||
snapshot.checkSendOrder(secondMessage, 30, 0) &&
|
snapshot.checkSendOrder(secondMessage, 30, 0) &&
|
||||||
snapshot.checkSendOrder(firstMessage, 30, 30)
|
snapshot.checkSendOrder(firstMessage, 30, 30)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ object MatrixPatterns {
|
|||||||
* - "@alice:domain.org".getDomain() will return "domain.org"
|
* - "@alice:domain.org".getDomain() will return "domain.org"
|
||||||
* - "@bob:domain.org:3455".getDomain() will return "domain.org:3455"
|
* - "@bob:domain.org:3455".getDomain() will return "domain.org:3455"
|
||||||
*/
|
*/
|
||||||
fun String.getDomain(): String {
|
fun String.getServerName(): String {
|
||||||
if (BuildConfig.DEBUG && !isUserId(this)) {
|
if (BuildConfig.DEBUG && !isUserId(this)) {
|
||||||
// They are some invalid userId localpart in the wild, but the domain part should be there anyway
|
// 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")
|
Timber.w("Not a valid user ID: $this")
|
||||||
|
@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) {
|
|||||||
MismatchedKeys("m.key_mismatch", "Key mismatch"),
|
MismatchedKeys("m.key_mismatch", "Key mismatch"),
|
||||||
UserError("m.user_error", "User error"),
|
UserError("m.user_error", "User error"),
|
||||||
MismatchedUser("m.user_mismatch", "User mismatch"),
|
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 {
|
fun safeValueOf(code: String?): CancelCode {
|
||||||
|
@ -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.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
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.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.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
@ -375,11 +376,11 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
|||||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||||
} else {
|
} else {
|
||||||
content.toModel<MessageContent>()?.relatesTo ?: run {
|
content.toModel<MessageContent>()?.relatesTo ?: run {
|
||||||
// Special case to handle stickers, while there is only a local msgtype for stickers
|
// Special cases when there is only a local msgtype for some event types
|
||||||
if (getClearType() == EventType.STICKER) {
|
when (getClearType()) {
|
||||||
getClearContent().toModel<MessageStickerContent>()?.relatesTo
|
EventType.STICKER -> getClearContent().toModel<MessageStickerContent>()?.relatesTo
|
||||||
} else {
|
in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel<MessageBeaconLocationDataContent>()?.relatesTo
|
||||||
null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
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.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.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
@ -140,6 +141,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
|||||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||||
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
|
||||||
|
in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import android.net.Uri
|
|||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
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.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
@ -381,7 +381,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||||||
|
|
||||||
return getWellknownTask.execute(
|
return getWellknownTask.execute(
|
||||||
GetWellknownTask.Params(
|
GetWellknownTask.Params(
|
||||||
domain = matrixId.getDomain(),
|
domain = matrixId.getServerName().substringBeforeLast(":"),
|
||||||
homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults()
|
homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -939,9 +939,25 @@ internal class DefaultVerificationService @Inject constructor(
|
|||||||
|
|
||||||
updatePendingRequest(
|
updatePendingRequest(
|
||||||
existingRequest.copy(
|
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<String> = 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? {
|
private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? {
|
||||||
|
@ -49,6 +49,11 @@ internal interface VerificationTransport {
|
|||||||
otherUserDeviceId: String?,
|
otherUserDeviceId: String?,
|
||||||
code: CancelCode)
|
code: CancelCode)
|
||||||
|
|
||||||
|
fun cancelTransaction(transactionId: String,
|
||||||
|
otherUserId: String,
|
||||||
|
otherUserDeviceIds: List<String>,
|
||||||
|
code: CancelCode)
|
||||||
|
|
||||||
fun done(transactionId: String,
|
fun done(transactionId: String,
|
||||||
onDone: (() -> Unit)?)
|
onDone: (() -> Unit)?)
|
||||||
|
|
||||||
|
@ -160,6 +160,9 @@ internal class VerificationTransportRoomMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List<String>, code: CancelCode) =
|
||||||
|
cancelTransaction(transactionId, otherUserId, null, code)
|
||||||
|
|
||||||
override fun done(transactionId: String,
|
override fun done(transactionId: String,
|
||||||
onDone: (() -> Unit)?) {
|
onDone: (() -> Unit)?) {
|
||||||
Timber.d("## SAS sending done for $transactionId")
|
Timber.d("## SAS sending done for $transactionId")
|
||||||
|
@ -193,6 +193,27 @@ internal class VerificationTransportToDevice(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List<String>, code: CancelCode) {
|
||||||
|
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||||
|
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
||||||
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
|
val messages = otherUserDeviceIds.associateWith { cancelMessage }
|
||||||
|
contentMap.setObjects(otherUserId, messages)
|
||||||
|
sendToDeviceTask
|
||||||
|
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
|
||||||
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
|
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,
|
override fun createAccept(tid: String,
|
||||||
keyAgreementProtocol: String,
|
keyAgreementProtocol: String,
|
||||||
hash: String,
|
hash: String,
|
||||||
|
@ -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.openid.DefaultOpenIdService
|
||||||
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
|
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.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.create.RoomCreateEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
@ -387,7 +385,4 @@ internal abstract class SessionModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
package org.matrix.android.sdk.internal.session.homeserver
|
package org.matrix.android.sdk.internal.session.homeserver
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
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.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
@ -93,10 +93,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.getOrNull()
|
}.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 {
|
val wellknownResult = runCatching {
|
||||||
getWellknownTask.execute(
|
getWellknownTask.execute(
|
||||||
GetWellknownTask.Params(
|
GetWellknownTask.Params(
|
||||||
domain = userId.getDomain(),
|
domain = userId.getServerName().substringBeforeLast(":"),
|
||||||
homeServerConnectionConfig = homeServerConnectionConfig
|
homeServerConnectionConfig = homeServerConnectionConfig
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.permalinks
|
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.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
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.events.model.toModel
|
||||||
@ -55,9 +55,9 @@ internal class ViaParameterFinder @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {
|
fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {
|
||||||
val userHomeserver = userId.getDomain()
|
val userHomeserver = userId.getServerName()
|
||||||
return getUserIdsOfJoinedMembers(roomId)
|
return getUserIdsOfJoinedMembers(roomId)
|
||||||
.map { it.getDomain() }
|
.map { it.getServerName() }
|
||||||
.groupBy { it }
|
.groupBy { it }
|
||||||
.mapValues { it.value.size }
|
.mapValues { it.value.size }
|
||||||
.toMutableMap()
|
.toMutableMap()
|
||||||
@ -92,7 +92,7 @@ internal class ViaParameterFinder @Inject constructor(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
return userThatCanInvite.map { it.getDomain() }
|
return userThatCanInvite.map { it.getServerName() }
|
||||||
.groupBy { it }
|
.groupBy { it }
|
||||||
.mapValues { it.value.size }
|
.mapValues { it.value.size }
|
||||||
.toMutableMap()
|
.toMutableMap()
|
||||||
|
@ -193,9 +193,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
in EventType.BEACON_LOCATION_DATA -> {
|
in EventType.BEACON_LOCATION_DATA -> {
|
||||||
event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let {
|
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
|
||||||
liveLocationAggregationProcessor.handleBeaconLocationData(realm, event, it, roomId, isLocalEcho)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
|
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
|
||||||
@ -260,6 +258,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
|
liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
in EventType.BEACON_LOCATION_DATA -> {
|
||||||
|
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
|
||||||
|
}
|
||||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
@ -756,4 +757,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||||||
verifSummary.sourceEvents.add(event.eventId)
|
verifSummary.sourceEvents.add(event.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
|
||||||
|
event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let {
|
||||||
|
liveLocationAggregationProcessor.handleBeaconLocationData(
|
||||||
|
realm,
|
||||||
|
event,
|
||||||
|
it,
|
||||||
|
roomId,
|
||||||
|
event.getRelationContent()?.eventId,
|
||||||
|
isLocalEcho
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<MessageBeaconLocationDataContent>()
|
|
||||||
?.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
|
|
||||||
}
|
|
@ -17,24 +17,83 @@
|
|||||||
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
|
package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
|
||||||
|
|
||||||
import io.realm.Realm
|
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.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.MessageBeaconInfoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
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 {
|
internal class LiveLocationAggregationProcessor @Inject constructor() {
|
||||||
fun handleBeaconInfo(
|
|
||||||
realm: Realm,
|
fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
|
||||||
event: Event,
|
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
|
||||||
content: MessageBeaconInfoContent,
|
return
|
||||||
roomId: String,
|
}
|
||||||
isLocalEcho: Boolean,
|
|
||||||
)
|
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(
|
fun handleBeaconLocationData(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
event: Event,
|
event: Event,
|
||||||
content: MessageBeaconLocationDataContent,
|
content: MessageBeaconLocationDataContent,
|
||||||
roomId: String,
|
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<MessageBeaconLocationDataContent>()
|
||||||
|
?.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
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.alias
|
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.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
|
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
@ -65,6 +65,6 @@ internal class RoomAliasAvailabilityChecker @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getDomain()
|
internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getServerName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,9 @@ echo "Search for forbidden patterns in code..."
|
|||||||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt \
|
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt \
|
||||||
./matrix-sdk-android/src/main/java \
|
./matrix-sdk-android/src/main/java \
|
||||||
./matrix-sdk-android-flow/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/main/java \
|
||||||
./vector/src/debug/java \
|
./vector/src/debug/java \
|
||||||
./vector/src/release/java \
|
./vector/src/release/java \
|
||||||
@ -100,6 +103,7 @@ echo
|
|||||||
echo "Search for forbidden patterns in resources..."
|
echo "Search for forbidden patterns in resources..."
|
||||||
|
|
||||||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt \
|
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt \
|
||||||
|
./library/ui-styles/src/main/res/values \
|
||||||
./vector/src/main/res/color \
|
./vector/src/main/res/color \
|
||||||
./vector/src/main/res/layout \
|
./vector/src/main/res/layout \
|
||||||
./vector/src/main/res/values \
|
./vector/src/main/res/values \
|
||||||
|
@ -450,9 +450,6 @@ dependencies {
|
|||||||
kapt libs.github.glideCompiler
|
kapt libs.github.glideCompiler
|
||||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||||
|
|
||||||
// Badge for compatibility
|
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
|
||||||
|
|
||||||
// Chat effects
|
// Chat effects
|
||||||
implementation 'nl.dionsegijn:konfetti-xml:2.0.2'
|
implementation 'nl.dionsegijn:konfetti-xml:2.0.2'
|
||||||
|
|
||||||
|
@ -104,11 +104,10 @@ class SpaceMenuRobot {
|
|||||||
|
|
||||||
fun leaveSpace() {
|
fun leaveSpace() {
|
||||||
clickOnSheet(R.id.leaveSpace)
|
clickOnSheet(R.id.leaveSpace)
|
||||||
waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
|
|
||||||
clickOn(R.id.leave_selected)
|
|
||||||
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
|
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
|
||||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||||
}
|
}
|
||||||
|
clickOn(R.id.spaceLeaveSelectAll)
|
||||||
clickOn(R.id.spaceLeaveButton)
|
clickOn(R.id.spaceLeaveButton)
|
||||||
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright 2019 New Vector Ltd
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.network.WifiDetector
|
import im.vector.app.core.network.WifiDetector
|
||||||
import im.vector.app.core.pushers.PushersManager
|
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.NotifiableEventResolver
|
||||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
@ -152,10 +148,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")
|
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()
|
val session = activeSessionHolder.getSafeActiveSession()
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
@ -369,11 +369,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
<br/>
|
<br/>
|
||||||
Copyright 2012 Square, Inc.
|
Copyright 2012 Square, Inc.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<b>ShortcutBadger</b>
|
|
||||||
<br/>
|
|
||||||
Copyright 2014 Leo Lin
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<b>diff-match-patch</b>
|
<b>diff-match-patch</b>
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -55,6 +55,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||||||
import im.vector.app.core.di.ActivityEntryPoint
|
import im.vector.app.core.di.ActivityEntryPoint
|
||||||
import im.vector.app.core.dialogs.DialogLocker
|
import im.vector.app.core.dialogs.DialogLocker
|
||||||
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
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.observeEvent
|
||||||
import im.vector.app.core.extensions.observeNotNull
|
import im.vector.app.core.extensions.observeNotNull
|
||||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
@ -611,11 +612,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
} else {
|
} else {
|
||||||
if (vectorPreferences.failFast()) {
|
fatalError("No CoordinatorLayout to display this snackbar!", vectorPreferences.failFast())
|
||||||
error("No CoordinatorLayout to display this snackbar!")
|
|
||||||
} else {
|
|
||||||
Timber.w("No CoordinatorLayout to display this snackbar!")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,27 +19,30 @@ package im.vector.app.core.resources
|
|||||||
import org.threeten.bp.Instant
|
import org.threeten.bp.Instant
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
import org.threeten.bp.ZoneId
|
import org.threeten.bp.ZoneId
|
||||||
|
import org.threeten.bp.ZoneOffset
|
||||||
|
|
||||||
object DateProvider {
|
object DateProvider {
|
||||||
|
|
||||||
private val zoneId = ZoneId.systemDefault()
|
// recompute the zoneId each time we access it to handle change of timezones
|
||||||
private val zoneOffset by lazy {
|
private val defaultZoneId: ZoneId
|
||||||
val now = currentLocalDateTime()
|
get() = ZoneId.systemDefault()
|
||||||
zoneId.rules.getOffset(now)
|
|
||||||
}
|
// 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 {
|
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
|
||||||
val instant = Instant.ofEpochMilli(timestamp ?: 0)
|
val instant = Instant.ofEpochMilli(timestamp ?: 0)
|
||||||
return LocalDateTime.ofInstant(instant, zoneId)
|
return LocalDateTime.ofInstant(instant, defaultZoneId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentLocalDateTime(): LocalDateTime {
|
fun currentLocalDateTime(): LocalDateTime {
|
||||||
val instant = Instant.now()
|
val instant = Instant.now()
|
||||||
return LocalDateTime.ofInstant(instant, zoneId)
|
return LocalDateTime.ofInstant(instant, defaultZoneId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTimestamp(localDateTime: LocalDateTime): Long {
|
fun toTimestamp(localDateTime: LocalDateTime): Long {
|
||||||
return localDateTime.toInstant(zoneOffset).toEpochMilli()
|
return localDateTime.toInstant(defaultZoneOffset).toEpochMilli()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,11 +19,15 @@ package im.vector.app.core.utils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
|
import im.vector.app.R
|
||||||
import org.threeten.bp.Duration
|
import org.threeten.bp.Duration
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
|
|
||||||
object TextUtils {
|
object TextUtils {
|
||||||
|
|
||||||
|
private const val MINUTES_PER_HOUR = 60
|
||||||
|
private const val SECONDS_PER_MINUTE = 60
|
||||||
|
|
||||||
private val suffixes = TreeMap<Int, String>().also {
|
private val suffixes = TreeMap<Int, String>().also {
|
||||||
it[1000] = "k"
|
it[1000] = "k"
|
||||||
it[1000000] = "M"
|
it[1000000] = "M"
|
||||||
@ -71,13 +75,63 @@ object TextUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun formatDuration(duration: Duration): String {
|
fun formatDuration(duration: Duration): String {
|
||||||
val hours = duration.seconds / 3600
|
val hours = getHours(duration)
|
||||||
val minutes = (duration.seconds % 3600) / 60
|
val minutes = getMinutes(duration)
|
||||||
val seconds = duration.seconds % 60
|
val seconds = getSeconds(duration)
|
||||||
return if (hours > 0) {
|
return if (hours > 0) {
|
||||||
String.format("%d:%02d:%02d", hours, minutes, seconds)
|
String.format("%d:%02d:%02d", hours, minutes, seconds)
|
||||||
} else {
|
} else {
|
||||||
String.format("%02d:%02d", minutes, seconds)
|
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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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.<br></br>
|
|
||||||
* The refresh is only effective if the device is:
|
|
||||||
* * offline * does not support FCM
|
|
||||||
* * FCM registration failed
|
|
||||||
* <br></br>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)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -148,7 +148,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private val locationPinProvider: LocationPinProvider,
|
private val locationPinProvider: LocationPinProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val urlMapProvider: UrlMapProvider,
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val liveLocationMessageItemFactory: LiveLocationMessageItemFactory,
|
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO inject this properly?
|
// TODO inject this properly?
|
||||||
@ -216,7 +216,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
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)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
return messageItem?.apply {
|
return messageItem?.apply {
|
||||||
@ -237,14 +237,14 @@ class MessageItemFactory @Inject constructor(
|
|||||||
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
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_()
|
return MessageLocationItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.locationUrl(locationUrl)
|
.locationUrl(locationUrl)
|
||||||
.mapWidth(width)
|
.mapWidth(width)
|
||||||
.mapHeight(height)
|
.mapHeight(height)
|
||||||
.userId(userId)
|
.locationUserId(locationUserId)
|
||||||
.locationPinProvider(locationPinProvider)
|
.locationPinProvider(locationPinProvider)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
@ -100,7 +100,7 @@ class TimelineItemFactory @Inject constructor(
|
|||||||
// Message itemsX
|
// Message itemsX
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
in EventType.POLL_START,
|
in EventType.POLL_START,
|
||||||
EventType.MESSAGE -> messageItemFactory.create(params)
|
EventType.MESSAGE -> messageItemFactory.create(params)
|
||||||
EventType.REDACTION,
|
EventType.REDACTION,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
@ -113,14 +113,15 @@ class TimelineItemFactory @Inject constructor(
|
|||||||
EventType.CALL_NEGOTIATE,
|
EventType.CALL_NEGOTIATE,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
in EventType.POLL_RESPONSE,
|
in EventType.POLL_RESPONSE,
|
||||||
in EventType.POLL_END -> noticeItemFactory.create(params)
|
in EventType.POLL_END,
|
||||||
|
in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params)
|
||||||
// Calls
|
// Calls
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_REJECT,
|
EventType.CALL_REJECT,
|
||||||
EventType.CALL_ANSWER -> callItemFactory.create(params)
|
EventType.CALL_ANSWER -> callItemFactory.create(params)
|
||||||
// Crypto
|
// Crypto
|
||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
// Redacted event, let the MessageItemFactory handle it
|
// Redacted event, let the MessageItemFactory handle it
|
||||||
messageItemFactory.create(params)
|
messageItemFactory.create(params)
|
||||||
@ -129,11 +130,11 @@ class TimelineItemFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
EventType.KEY_VERIFICATION_DONE -> {
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
verificationConclusionItemFactory.create(params)
|
verificationConclusionItemFactory.create(params)
|
||||||
}
|
}
|
||||||
// Unhandled event types
|
// Unhandled event types
|
||||||
else -> {
|
else -> {
|
||||||
// Should only happen when shouldShowHiddenEvents() settings is ON
|
// Should only happen when shouldShowHiddenEvents() settings is ON
|
||||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||||
defaultItemFactory.create(params)
|
defaultItemFactory.create(params)
|
||||||
|
@ -107,7 +107,8 @@ class NoticeEventFormatter @Inject constructor(
|
|||||||
EventType.REDACTION,
|
EventType.REDACTION,
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
in EventType.POLL_RESPONSE,
|
in EventType.POLL_RESPONSE,
|
||||||
in EventType.POLL_END -> formatDebug(timelineEvent.root)
|
in EventType.POLL_END,
|
||||||
|
in EventType.BEACON_LOCATION_DATA -> formatDebug(timelineEvent.root)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("Type $type not handled by this formatter")
|
Timber.v("Type $type not handled by this formatter")
|
||||||
null
|
null
|
||||||
|
@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
|
|||||||
import javax.inject.Inject
|
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.
|
* This class is responsible of building extra information data associated to a given event.
|
||||||
* TODO Update this comment
|
|
||||||
*/
|
*/
|
||||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
@ -119,7 +118,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
isFirstFromThisSender = isFirstFromThisSender,
|
isFirstFromThisSender = isFirstFromThisSender,
|
||||||
isLastFromThisSender = isLastFromThisSender,
|
isLastFromThisSender = isLastFromThisSender,
|
||||||
e2eDecoration = e2eDecoration,
|
e2eDecoration = e2eDecoration,
|
||||||
sendStateDecoration = sendStateDecoration
|
sendStateDecoration = sendStateDecoration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ class MessageItemAttributesFactory @Inject constructor(
|
|||||||
memberClickListener = {
|
memberClickListener = {
|
||||||
callback?.onMemberNameClicked(informationData)
|
callback?.onMemberNameClicked(informationData)
|
||||||
},
|
},
|
||||||
|
callback = callback,
|
||||||
reactionPillCallback = callback,
|
reactionPillCallback = callback,
|
||||||
avatarCallback = callback,
|
avatarCallback = callback,
|
||||||
threadCallback = callback,
|
threadCallback = callback,
|
||||||
|
@ -178,6 +178,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||||||
override val itemLongClickListener: View.OnLongClickListener? = null,
|
override val itemLongClickListener: View.OnLongClickListener? = null,
|
||||||
override val itemClickListener: ClickListener? = null,
|
override val itemClickListener: ClickListener? = null,
|
||||||
val memberClickListener: ClickListener? = null,
|
val memberClickListener: ClickListener? = null,
|
||||||
|
val callback: TimelineEventController.Callback? = null,
|
||||||
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
||||||
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
||||||
val threadCallback: TimelineEventController.ThreadCallback? = null,
|
val threadCallback: TimelineEventController.ThreadCallback? = null,
|
||||||
|
@ -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<H : AbsMessageLocationItem.Holder> : AbsMessageItem<H>() {
|
||||||
|
|
||||||
|
@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<Drawable> {
|
||||||
|
override fun onLoadFailed(e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<Drawable>?,
|
||||||
|
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<Drawable>?,
|
||||||
|
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<ImageView>(R.id.staticMapImageView)
|
||||||
|
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
|
||||||
|
val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -42,7 +42,7 @@ data class MessageInformationData(
|
|||||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||||
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
|
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
|
||||||
val isFirstFromThisSender: Boolean = false,
|
val isFirstFromThisSender: Boolean = false,
|
||||||
val isLastFromThisSender: Boolean = false
|
val isLastFromThisSender: Boolean = false,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
val matrixItem: MatrixItem
|
val matrixItem: MatrixItem
|
||||||
|
@ -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<MessageLiveLocationInactiveItem.Holder>(),
|
||||||
|
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<ImageView>(R.id.locationLiveInactiveBanner)
|
||||||
|
val noLocationMapImageView by bind<ImageView>(R.id.locationLiveInactiveMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STUB_ID = R.id.messageContentLiveLocationInactiveStub
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageLiveLocationItem.Holder>() {
|
||||||
|
|
||||||
|
@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<LocationLiveMessageBannerView>(R.id.locationLiveMessageBanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STUB_ID = R.id.messageContentLiveLocationStub
|
||||||
|
}
|
||||||
|
}
|
@ -16,22 +16,15 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
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.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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageLiveLocationStartItem : AbsMessageItem<MessageLiveLocationStartItem.Holder>() {
|
abstract class MessageLiveLocationStartItem :
|
||||||
|
AbsMessageItem<MessageLiveLocationStartItem.Holder>(),
|
||||||
|
LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var mapWidth: Int = 0
|
var mapWidth: Int = 0
|
||||||
@ -42,44 +35,8 @@ abstract class MessageLiveLocationStartItem : AbsMessageItem<MessageLiveLocation
|
|||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
renderSendState(holder.view, null)
|
renderSendState(holder.view, null)
|
||||||
bindMap(holder)
|
bindMap(holder.noLocationMapImageView, mapWidth, mapHeight, attributes.informationData.messageLayout)
|
||||||
bindBottomBanner(holder)
|
bindBottomBanner(holder.bannerImageView, attributes.informationData.messageLayout)
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindMap(holder: Holder) {
|
|
||||||
val messageLayout = attributes.informationData.messageLayout
|
|
||||||
val mapCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
|
|
||||||
messageLayout.cornersRadius.granularRoundedCorners()
|
|
||||||
} else {
|
|
||||||
RoundedCorners(getDefaultLayoutCornerRadiusInDp(holder))
|
|
||||||
}
|
|
||||||
holder.noLocationMapImageView.updateLayoutParams {
|
|
||||||
width = mapWidth
|
|
||||||
height = mapHeight
|
|
||||||
}
|
|
||||||
GlideApp.with(holder.noLocationMapImageView)
|
|
||||||
.load(R.drawable.bg_no_location_map)
|
|
||||||
.transform(mapCornerTransformation)
|
|
||||||
.into(holder.noLocationMapImageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindBottomBanner(holder: Holder) {
|
|
||||||
val messageLayout = attributes.informationData.messageLayout
|
|
||||||
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
|
|
||||||
GranularRoundedCorners(0f, 0f, messageLayout.cornersRadius.bottomEndRadius, messageLayout.cornersRadius.bottomStartRadius)
|
|
||||||
} else {
|
|
||||||
val bottomCornerRadius = getDefaultLayoutCornerRadiusInDp(holder).toFloat()
|
|
||||||
GranularRoundedCorners(0f, 0f, bottomCornerRadius, bottomCornerRadius)
|
|
||||||
}
|
|
||||||
GlideApp.with(holder.bannerImageView)
|
|
||||||
.load(ColorDrawable(ThemeUtils.getColor(holder.bannerImageView.context, R.attr.colorSurface)))
|
|
||||||
.transform(imageCornerTransformation)
|
|
||||||
.into(holder.bannerImageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDefaultLayoutCornerRadiusInDp(holder: Holder): Int {
|
|
||||||
val dimensionConverter = DimensionConverter(holder.view.resources)
|
|
||||||
return dimensionConverter.dpToPx(8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 New Vector Ltd
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,97 +16,15 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
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.core.view.isVisible
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
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.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
|
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
abstract class MessageLocationItem : AbsMessageLocationItem<MessageLocationItem.Holder>() {
|
||||||
|
|
||||||
@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<Drawable> {
|
|
||||||
override fun onLoadFailed(e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<Drawable>?,
|
|
||||||
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<Drawable>?,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageLocationItem.Holder(STUB_ID)
|
||||||
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
|
||||||
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
|
|
||||||
val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentLocationStub
|
private const val STUB_ID = R.id.messageContentLocationStub
|
||||||
|
@ -66,6 +66,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||||||
MessageType.MSGTYPE_VIDEO,
|
MessageType.MSGTYPE_VIDEO,
|
||||||
MessageType.MSGTYPE_BEACON_INFO,
|
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 {
|
private val cornerRadius: Float by lazy {
|
||||||
@ -145,9 +150,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageContent?.timestampInsideMessage(): Boolean {
|
private fun MessageContent?.timestampInsideMessage(): Boolean {
|
||||||
if (this == null) return false
|
return when {
|
||||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
this == null -> false
|
||||||
return this.msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
|
msgType in MSG_TYPES_WITH_LOCATION_DATA -> vectorPreferences.labsRenderLocationsInTimeline()
|
||||||
|
else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageContent?.shouldAddMessageOverlay(): Boolean {
|
private fun MessageContent?.shouldAddMessageOverlay(): Boolean {
|
||||||
|
@ -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<SpaceDirectoryFilterNoResults.Holder>() {
|
||||||
|
class Holder : VectorEpoxyHolder()
|
||||||
|
}
|
@ -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_PREVIEW = 15.0
|
||||||
const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.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
|
const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f
|
||||||
|
@ -29,7 +29,7 @@ data class LocationData(
|
|||||||
) : Parcelable
|
) : 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)
|
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||||
* @return location data or null if geo uri is not valid
|
* @return location data or null if geo uri is not valid
|
||||||
*/
|
*/
|
||||||
@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? {
|
|||||||
return parseGeo(getBestGeoUri())
|
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
|
@VisibleForTesting
|
||||||
fun parseGeo(geo: String): LocationData? {
|
fun parseGeo(geo: String): LocationData? {
|
||||||
val geoParts = geo
|
val geoParts = geo
|
||||||
|
@ -55,7 +55,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
private var roomArgsList = mutableListOf<RoomArgs>()
|
/**
|
||||||
|
* Keep track of a map between beacon event Id starting the live and RoomArgs.
|
||||||
|
*/
|
||||||
|
private var roomArgsMap = mutableMapOf<String, RoomArgs>()
|
||||||
private var timers = mutableListOf<Timer>()
|
private var timers = mutableListOf<Timer>()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -73,8 +76,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
||||||
|
|
||||||
if (roomArgs != null) {
|
if (roomArgs != null) {
|
||||||
roomArgsList.add(roomArgs)
|
|
||||||
|
|
||||||
// Show a sticky notification
|
// Show a sticky notification
|
||||||
val notification = notificationUtils.buildLiveLocationSharingNotification()
|
val notification = notificationUtils.buildLiveLocationSharingNotification()
|
||||||
startForeground(roomArgs.roomId.hashCode(), notification)
|
startForeground(roomArgs.roomId.hashCode(), notification)
|
||||||
@ -87,7 +88,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
.getSafeActiveSession()
|
.getSafeActiveSession()
|
||||||
?.let { session ->
|
?.let { session ->
|
||||||
session.coroutineScope.launch(session.coroutineDispatchers.io) {
|
session.coroutineScope.launch(session.coroutineDispatchers.io) {
|
||||||
sendLiveBeaconInfo(session, roomArgs)
|
sendStartingLiveBeaconInfo(session, roomArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
|
private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
|
||||||
val beaconContent = MessageBeaconInfoContent(
|
val beaconContent = MessageBeaconInfoContent(
|
||||||
timeout = roomArgs.durationMillis,
|
timeout = roomArgs.durationMillis,
|
||||||
isLive = true,
|
isLive = true,
|
||||||
@ -103,7 +104,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
).toContent()
|
).toContent()
|
||||||
|
|
||||||
val stateKey = session.myUserId
|
val stateKey = session.myUserId
|
||||||
session
|
val beaconEventId = session
|
||||||
.getRoom(roomArgs.roomId)
|
.getRoom(roomArgs.roomId)
|
||||||
?.stateService()
|
?.stateService()
|
||||||
?.sendStateEvent(
|
?.sendStateEvent(
|
||||||
@ -111,6 +112,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
stateKey = stateKey,
|
stateKey = stateKey,
|
||||||
body = beaconContent
|
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) {
|
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
|
// Send a new beacon info state by setting live field as false
|
||||||
sendStoppedBeaconInfo(roomId)
|
sendStoppedBeaconInfo(roomId)
|
||||||
|
|
||||||
synchronized(roomArgsList) {
|
synchronized(roomArgsMap) {
|
||||||
roomArgsList.removeAll { it.roomId == roomId }
|
val beaconIds = roomArgsMap
|
||||||
if (roomArgsList.isEmpty()) {
|
.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")
|
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
|
||||||
destroyMe()
|
destroyMe()
|
||||||
}
|
}
|
||||||
@ -156,16 +171,17 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
override fun onLocationUpdate(locationData: LocationData) {
|
override fun onLocationUpdate(locationData: LocationData) {
|
||||||
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
||||||
|
|
||||||
val session = activeSessionHolder.getSafeActiveSession()
|
|
||||||
// Emit location update to all rooms in which live location sharing is active
|
// Emit location update to all rooms in which live location sharing is active
|
||||||
session?.coroutineScope?.launch(session.coroutineDispatchers.io) {
|
roomArgsMap.toMap().forEach { item ->
|
||||||
roomArgsList.toList().forEach { roomArg ->
|
sendLiveLocation(item.value.roomId, item.key, locationData)
|
||||||
sendLiveLocation(roomArg.roomId, locationData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendLiveLocation(roomId: String, locationData: LocationData) {
|
private fun sendLiveLocation(
|
||||||
|
roomId: String,
|
||||||
|
beaconInfoEventId: String,
|
||||||
|
locationData: LocationData
|
||||||
|
) {
|
||||||
val session = activeSessionHolder.getSafeActiveSession()
|
val session = activeSessionHolder.getSafeActiveSession()
|
||||||
val room = session?.getRoom(roomId)
|
val room = session?.getRoom(roomId)
|
||||||
val userId = session?.myUserId
|
val userId = session?.myUserId
|
||||||
@ -174,18 +190,12 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
room
|
room.sendService().sendLiveLocation(
|
||||||
.stateService()
|
beaconInfoEventId = beaconInfoEventId,
|
||||||
.getLiveLocationBeaconInfo(userId, true)
|
latitude = locationData.latitude,
|
||||||
?.eventId
|
longitude = locationData.longitude,
|
||||||
?.let {
|
uncertainty = locationData.uncertainty
|
||||||
room.sendService().sendLiveLocation(
|
)
|
||||||
beaconInfoEventId = it,
|
|
||||||
latitude = locationData.latitude,
|
|
||||||
longitude = locationData.longitude,
|
|
||||||
uncertainty = locationData.uncertainty
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationProviderIsNotAvailable() {
|
override fun onLocationProviderIsNotAvailable() {
|
||||||
|
@ -40,10 +40,12 @@ class LocationTracker @Inject constructor(
|
|||||||
fun onLocationProviderIsNotAvailable()
|
fun onLocationProviderIsNotAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
private var hasGpsProviderLiveLocation = false
|
private var hasGpsProviderLiveLocation = false
|
||||||
|
|
||||||
|
private var lastLocation: LocationData? = null
|
||||||
|
|
||||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||||
fun start() {
|
fun start() {
|
||||||
Timber.d("## LocationTracker. start()")
|
Timber.d("## LocationTracker. start()")
|
||||||
@ -92,6 +94,14 @@ class LocationTracker @Inject constructor(
|
|||||||
callbacks.clear()
|
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) {
|
fun addCallback(callback: Callback) {
|
||||||
if (!callbacks.contains(callback)) {
|
if (!callbacks.contains(callback)) {
|
||||||
callbacks.add(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) {
|
override fun onProviderDisabled(provider: String) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -36,7 +36,7 @@ import im.vector.app.core.resources.StringProvider
|
|||||||
import im.vector.app.core.utils.ensureTrailingSlash
|
import im.vector.app.core.utils.ensureTrailingSlash
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
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.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
@ -607,7 +607,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||||
)
|
)
|
||||||
?: HomeServerConnectionConfig(
|
?: HomeServerConnectionConfig(
|
||||||
homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
|
homeServerUri = Uri.parse("https://${action.username.getServerName()}"),
|
||||||
homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
|
homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||||
)
|
)
|
||||||
|
@ -38,7 +38,7 @@ import im.vector.app.features.login.LoginMode
|
|||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
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.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
@ -640,7 +640,7 @@ class LoginViewModel2 @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
viewEvent?.let { _viewEvents.post(it) }
|
viewEvent?.let { _viewEvents.post(it) }
|
||||||
|
|
||||||
val urlFromUser = action.username.getDomain()
|
val urlFromUser = action.username.getServerName()
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
@ -317,6 +317,7 @@ class DefaultNavigator @Inject constructor(
|
|||||||
if (context is AppCompatActivity) {
|
if (context is AppCompatActivity) {
|
||||||
if (context !is MatrixToBottomSheet.InteractionListener) {
|
if (context !is MatrixToBottomSheet.InteractionListener) {
|
||||||
fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast())
|
fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// TODO check if there is already one??
|
// TODO check if there is already one??
|
||||||
MatrixToBottomSheet.withLink(link, origin)
|
MatrixToBottomSheet.withLink(link, origin)
|
||||||
|
@ -20,7 +20,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.extensions.andThen
|
import im.vector.app.core.extensions.andThen
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
|
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.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
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(
|
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),
|
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
|
||||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||||
)
|
)
|
||||||
|
@ -16,14 +16,14 @@
|
|||||||
|
|
||||||
package im.vector.app.features.raw.wellknown
|
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.auth.data.SessionParams
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
|
||||||
suspend fun RawService.getElementWellknown(sessionParams: SessionParams): ElementWellKnown? {
|
suspend fun RawService.getElementWellknown(sessionParams: SessionParams): ElementWellKnown? {
|
||||||
// By default we use the domain of the userId to retrieve the .well-known data
|
// 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) }
|
return tryOrNull { getWellknown(domain) }
|
||||||
?.let { ElementWellKnownMapper.from(it) }
|
?.let { ElementWellKnownMapper.from(it) }
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
|
|||||||
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
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.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
@ -98,7 +98,7 @@ class CreateRoomViewModel @AssistedInject constructor(
|
|||||||
private fun initHomeServerName() {
|
private fun initHomeServerName() {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
homeServerName = session.myUserId.getDomain()
|
homeServerName = session.myUserId.getServerName()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.resources.StringArrayProvider
|
import im.vector.app.core.resources.StringArrayProvider
|
||||||
import im.vector.app.features.roomdirectory.RoomDirectoryData
|
import im.vector.app.features.roomdirectory.RoomDirectoryData
|
||||||
import im.vector.app.features.roomdirectory.RoomDirectoryServer
|
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.Session
|
||||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -37,7 +37,7 @@ class RoomDirectoryListCreator @Inject constructor(
|
|||||||
val protocols = ArrayList<RoomDirectoryData>()
|
val protocols = ArrayList<RoomDirectoryData>()
|
||||||
|
|
||||||
// Add user homeserver name
|
// Add user homeserver name
|
||||||
val userHsName = session.myUserId.getDomain()
|
val userHsName = session.myUserId.getServerName()
|
||||||
|
|
||||||
// Add default protocol
|
// Add default protocol
|
||||||
protocols.add(
|
protocols.add(
|
||||||
|
@ -31,7 +31,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
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.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.EventType
|
||||||
@ -96,7 +96,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo
|
|||||||
private fun initHomeServerName() {
|
private fun initHomeServerName() {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
homeServerName = session.myUserId.getDomain()
|
homeServerName = session.myUserId.getServerName()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<BottomSheetLeaveSpaceBinding>() {
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,6 +35,7 @@ import im.vector.app.features.home.AvatarRenderer
|
|||||||
import im.vector.app.features.navigation.Navigator
|
import im.vector.app.features.navigation.Navigator
|
||||||
import im.vector.app.features.rageshake.BugReporter
|
import im.vector.app.features.rageshake.BugReporter
|
||||||
import im.vector.app.features.roomprofile.RoomProfileActivity
|
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.ManageType
|
||||||
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@ -109,7 +110,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
|
|||||||
}
|
}
|
||||||
|
|
||||||
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {
|
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||||
LeaveSpaceBottomSheet.newInstance(spaceArgs.spaceId).show(childFragmentManager, "LOGOUT")
|
startActivity(SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ import im.vector.app.core.resources.StringProvider
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
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.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
|
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
|
||||||
import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult
|
import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult
|
||||||
@ -66,7 +66,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
|
|||||||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
homeServerName = session.myUserId.getDomain(),
|
homeServerName = session.myUserId.getServerName(),
|
||||||
canInviteByMail = identityServerUrl != null
|
canInviteByMail = identityServerUrl != null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,8 @@ import im.vector.app.core.ui.list.genericEmptyWithActionItem
|
|||||||
import im.vector.app.core.ui.list.genericPillItem
|
import im.vector.app.core.ui.list.genericPillItem
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.list.spaceChildInfoItem
|
import im.vector.app.features.home.room.list.spaceChildInfoItem
|
||||||
|
import im.vector.app.features.home.room.list.spaceDirectoryFilterNoResults
|
||||||
|
import im.vector.app.features.spaces.manage.SpaceChildInfoMatchFilter
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
@ -53,6 +55,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
) : TypedEpoxyController<SpaceDirectoryState>() {
|
) : TypedEpoxyController<SpaceDirectoryState>() {
|
||||||
|
|
||||||
interface InteractionListener {
|
interface InteractionListener {
|
||||||
|
fun onFilterQueryChanged(query: String?)
|
||||||
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
|
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
|
||||||
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
|
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
|
||||||
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
|
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
|
||||||
@ -62,6 +65,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var listener: InteractionListener? = null
|
var listener: InteractionListener? = null
|
||||||
|
private val matchFilter = SpaceChildInfoMatchFilter()
|
||||||
|
|
||||||
override fun buildModels(data: SpaceDirectoryState?) {
|
override fun buildModels(data: SpaceDirectoryState?) {
|
||||||
val host = this
|
val host = this
|
||||||
@ -76,7 +80,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
val failure = results.error
|
val failure = results.error
|
||||||
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
|
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
|
||||||
genericPillItem {
|
genericPillItem {
|
||||||
id("HS no Support")
|
id("hs_no_support")
|
||||||
imageRes(R.drawable.error)
|
imageRes(R.drawable.error)
|
||||||
tintIcon(false)
|
tintIcon(false)
|
||||||
text(
|
text(
|
||||||
@ -132,43 +136,52 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
flattenChildInfo.forEach { info ->
|
matchFilter.filter = data?.currentFilter ?: ""
|
||||||
val isSpace = info.roomType == RoomType.SPACE
|
val filteredChildInfo = flattenChildInfo.filter { matchFilter.test(it) }
|
||||||
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 {
|
if (filteredChildInfo.isEmpty()) {
|
||||||
id(info.childRoomId)
|
spaceDirectoryFilterNoResults {
|
||||||
matrixItem(matrixItem)
|
id("no_results")
|
||||||
avatarRenderer(host.avatarRenderer)
|
}
|
||||||
topic(info.topic)
|
} else {
|
||||||
suggested(info.suggested.orFalse())
|
filteredChildInfo.forEach { info ->
|
||||||
errorLabel(
|
val isSpace = info.roomType == RoomType.SPACE
|
||||||
error?.let {
|
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
|
||||||
host.stringProvider.getString(R.string.error_failed_to_join_room, host.errorFormatter.toHumanReadable(it))
|
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) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.databinding.FragmentSpaceDirectoryBinding
|
||||||
import im.vector.app.features.analytics.plan.MobileScreen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
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.permalink.PermalinkHandler
|
||||||
import im.vector.app.features.spaces.manage.ManageType
|
import im.vector.app.features.spaces.manage.ManageType
|
||||||
import im.vector.app.features.spaces.manage.SpaceAddRoomSpaceChooserBottomSheet
|
import im.vector.app.features.spaces.manage.SpaceAddRoomSpaceChooserBottomSheet
|
||||||
@ -63,7 +63,6 @@ data class SpaceDirectoryArgs(
|
|||||||
class SpaceDirectoryFragment @Inject constructor(
|
class SpaceDirectoryFragment @Inject constructor(
|
||||||
private val epoxyController: SpaceDirectoryController,
|
private val epoxyController: SpaceDirectoryController,
|
||||||
private val permalinkHandler: PermalinkHandler,
|
private val permalinkHandler: PermalinkHandler,
|
||||||
private val spaceCardRenderer: SpaceCardRenderer,
|
|
||||||
private val colorProvider: ColorProvider
|
private val colorProvider: ColorProvider
|
||||||
) : VectorBaseFragment<FragmentSpaceDirectoryBinding>(),
|
) : VectorBaseFragment<FragmentSpaceDirectoryBinding>(),
|
||||||
SpaceDirectoryController.InteractionListener,
|
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
|
// Hide FAB when list is scrolling
|
||||||
views.spaceDirectoryList.addOnScrollListener(
|
views.spaceDirectoryList.addOnScrollListener(
|
||||||
object : RecyclerView.OnScrollListener() {
|
object : RecyclerView.OnScrollListener() {
|
||||||
@ -167,18 +163,37 @@ class SpaceDirectoryFragment @Inject constructor(
|
|||||||
// it's the root
|
// it's the root
|
||||||
toolbar?.setTitle(R.string.space_explore_activity_title)
|
toolbar?.setTitle(R.string.space_explore_activity_title)
|
||||||
} else {
|
} else {
|
||||||
toolbar?.title = state.currentRootSummary?.name
|
val spaceName = state.currentRootSummary?.name
|
||||||
?: state.currentRootSummary?.canonicalAlias
|
?: 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
|
views.addOrCreateChatRoomButton.isVisible = state.canAddRooms
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
|
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
|
||||||
menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms
|
menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms
|
||||||
menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented
|
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)
|
super.onPrepareOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +213,10 @@ class SpaceDirectoryFragment @Inject constructor(
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFilterQueryChanged(query: String?) {
|
||||||
|
viewModel.handle(SpaceDirectoryViewAction.FilterRooms(query))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
|
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
|
||||||
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
|
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
|||||||
sealed class SpaceDirectoryViewAction : VectorViewModelAction {
|
sealed class SpaceDirectoryViewAction : VectorViewModelAction {
|
||||||
data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
||||||
data class JoinOrOpen(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 ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
||||||
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
|
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
|
||||||
object CreateNewRoom : SpaceDirectoryViewAction()
|
object CreateNewRoom : SpaceDirectoryViewAction()
|
||||||
|
@ -225,9 +225,16 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToCreateNewRoom(state.currentRootSummary?.roomId ?: initialState.spaceId))
|
_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 ->
|
private fun handleBack() = withState { state ->
|
||||||
if (state.hierarchyStack.isEmpty()) {
|
if (state.hierarchyStack.isEmpty()) {
|
||||||
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
|
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
|
||||||
|
@ -21,6 +21,9 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||||||
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
|
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
|
||||||
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
|
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
|
||||||
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
|
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
|
||||||
|
data class SetFilteringEnabled(val isEnabled: Boolean) : SpaceLeaveAdvanceViewAction()
|
||||||
object DoLeave : SpaceLeaveAdvanceViewAction()
|
object DoLeave : SpaceLeaveAdvanceViewAction()
|
||||||
object ClearError : SpaceLeaveAdvanceViewAction()
|
object ClearError : SpaceLeaveAdvanceViewAction()
|
||||||
|
object SelectAll : SpaceLeaveAdvanceViewAction()
|
||||||
|
object SelectNone : SpaceLeaveAdvanceViewAction()
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,11 @@ data class SpaceLeaveAdvanceViewState(
|
|||||||
val allChildren: Async<List<RoomSummary>> = Uninitialized,
|
val allChildren: Async<List<RoomSummary>> = Uninitialized,
|
||||||
val selectedRooms: List<String> = emptyList(),
|
val selectedRooms: List<String> = emptyList(),
|
||||||
val currentFilter: String = "",
|
val currentFilter: String = "",
|
||||||
val leaveState: Async<Unit> = Uninitialized
|
val leaveState: Async<Unit> = Uninitialized,
|
||||||
|
val isFilteringEnabled: Boolean = false,
|
||||||
|
val isLastAdmin: Boolean = false
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: SpaceBottomSheetSettingsArgs) : this(
|
constructor(args: SpaceBottomSheetSettingsArgs) : this(
|
||||||
spaceId = args.spaceId
|
spaceId = args.spaceId
|
||||||
)
|
)
|
||||||
|
@ -18,20 +18,23 @@ package im.vector.app.features.spaces.leave
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.activityViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior
|
||||||
import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding
|
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 org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SpaceLeaveAdvancedFragment @Inject constructor(
|
class SpaceLeaveAdvancedFragment @Inject constructor(
|
||||||
@ -44,11 +47,33 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||||||
|
|
||||||
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
|
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
|
||||||
|
|
||||||
|
override fun getMenuRes() = R.menu.menu_space_leave
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupToolbar(views.toolbar)
|
|
||||||
.allowBack()
|
|
||||||
controller.listener = this
|
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.roomList.configureWith(controller)
|
||||||
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
|
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
|
||||||
|
|
||||||
@ -56,12 +81,23 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||||||
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
|
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
|
||||||
}
|
}
|
||||||
|
|
||||||
views.publicRoomsFilter.queryTextChanges()
|
views.spaceLeaveSelectGroup.setOnCheckedChangeListener { _, optionId ->
|
||||||
.debounce(100)
|
when (optionId) {
|
||||||
.onEach {
|
R.id.spaceLeaveSelectAll -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectAll)
|
||||||
viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString()))
|
R.id.spaceLeaveSelectNone -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectNone)
|
||||||
}
|
}
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
override fun onDestroyView() {
|
||||||
@ -72,10 +108,63 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
|
|
||||||
|
if (state.isFilteringEnabled) {
|
||||||
|
views.appBarLayout.setExpanded(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAppBarBehaviorState(state)
|
||||||
|
updateRadioButtonsState(state)
|
||||||
|
|
||||||
controller.setData(state)
|
controller.setData(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemSelected(roomSummary: RoomSummary) {
|
override fun onItemSelected(roomSummary: RoomSummary) {
|
||||||
viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId))
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,9 +36,14 @@ import okhttp3.internal.toImmutableList
|
|||||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.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.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.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
import org.matrix.android.sdk.flow.unwrap
|
import org.matrix.android.sdk.flow.unwrap
|
||||||
@ -50,52 +55,24 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
|
|||||||
private val appStateHandler: AppStateHandler
|
private val appStateHandler: AppStateHandler
|
||||||
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(initialState) {
|
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state ->
|
init {
|
||||||
when (action) {
|
val space = session.getRoom(initialState.spaceId)
|
||||||
is SpaceLeaveAdvanceViewAction.ToggleSelection -> {
|
val spaceSummary = space?.roomSummary()
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.spaceService().leaveSpace(initialState.spaceId)
|
val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
||||||
// We observe the membership and to dismiss when we have remote echo of leaving
|
powerLevelsEvent?.content?.toModel<PowerLevelsContent>()?.let { powerLevelsContent ->
|
||||||
} catch (failure: Throwable) {
|
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
|
||||||
setState { copy(leaveState = Fail(failure)) }
|
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
|
||||||
}
|
val otherAdminCount = spaceSummary?.otherMemberIds
|
||||||
}
|
?.map { powerLevelsHelper.getUserRole(it) }
|
||||||
}
|
?.count { it is Role.Admin }
|
||||||
SpaceLeaveAdvanceViewAction.ClearError -> {
|
?: 0
|
||||||
setState { copy(leaveState = Uninitialized) }
|
val isLastAdmin = isAdmin && otherAdminCount == 0
|
||||||
|
setState {
|
||||||
|
copy(isLastAdmin = isLastAdmin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val spaceSummary = session.getRoomSummary(initialState.spaceId)
|
|
||||||
setState { copy(spaceSummary = spaceSummary) }
|
setState { copy(spaceSummary = spaceSummary) }
|
||||||
session.getRoom(initialState.spaceId)?.let { room ->
|
session.getRoom(initialState.spaceId)?.let { room ->
|
||||||
room.flow().liveRoomSummary()
|
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
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<SpaceLeaveAdvancedViewModel, SpaceLeaveAdvanceViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<SpaceLeaveAdvancedViewModel, SpaceLeaveAdvanceViewState> {
|
||||||
override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel
|
override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel
|
||||||
|
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 876 B |
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 594 B |
BIN
vector/src/main/res/drawable-night-hdpi/bg_no_location_map.webp
Normal file
After Width: | Height: | Size: 958 B |
BIN
vector/src/main/res/drawable-night-mdpi/bg_no_location_map.webp
Normal file
After Width: | Height: | Size: 640 B |
BIN
vector/src/main/res/drawable-night-xhdpi/bg_no_location_map.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.4 KiB |
@ -1,105 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?android:colorBackground"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bottom_leave_space_warning_text"
|
|
||||||
style="@style/Widget.Vector.TextView.Subtitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
|
||||||
android:layout_marginTop="20dp"
|
|
||||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:textColor="?vctr_content_primary"
|
|
||||||
tools:text="@string/space_leave_prompt_msg_with_name" />
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/autoLeaveRadioGroup"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="?dialogPreferredPadding"
|
|
||||||
android:paddingTop="12dp"
|
|
||||||
android:paddingEnd="?dialogPreferredPadding"
|
|
||||||
android:paddingBottom="12dp">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/leave_all"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/leave_all_rooms_and_spaces"
|
|
||||||
tools:checked="true" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/leave_none"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="180dp"
|
|
||||||
android:text="@string/dont_leave_any" />
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/leave_selected"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minWidth="180dp"
|
|
||||||
android:text="@string/leave_specific_ones" />
|
|
||||||
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/inlineErrorText"
|
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
|
||||||
android:textColor="?colorError"
|
|
||||||
tools:visibility="visible"
|
|
||||||
tools:text="@string/error_no_network"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="20dp"
|
|
||||||
android:layout_marginEnd="16dp">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/leaveProgress"
|
|
||||||
style="?android:attr/progressBarStyle"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/leaveButton"
|
|
||||||
style="@style/Widget.Vector.Button.Destructive"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="@string/leave_space" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/cancelButton"
|
|
||||||
style="@style/Widget.Vector.Button.Text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:text="@string/action_cancel" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -11,35 +11,12 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
|
||||||
android:id="@+id/spaceExploreCollapsingToolbarLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:contentScrim="?android:colorBackground"
|
|
||||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
|
|
||||||
app:scrimAnimationDuration="250"
|
|
||||||
app:scrimVisibleHeightTrigger="120dp"
|
|
||||||
app:titleEnabled="false"
|
|
||||||
app:toolbarId="@id/toolbar">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="40dp">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/spaceCard"
|
|
||||||
layout="@layout/fragment_matrix_to_room_space_card" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:layout_collapseMode="pin" />
|
app:contentInsetStart="0dp">
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
@ -57,7 +34,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="16dp "
|
android:layout_marginBottom="16dp"
|
||||||
android:contentDescription="@string/a11y_create_room"
|
android:contentDescription="@string/a11y_create_room"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:src="@drawable/ic_fab_add"
|
android:src="@drawable/ic_fab_add"
|
||||||
|
@ -16,41 +16,107 @@
|
|||||||
tools:listitem="@layout/item_room_to_add_in_space" />
|
tools:listitem="@layout/item_room_to_add_in_space" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBarLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_behavior="im.vector.app.core.utils.ToggleableAppBarLayoutBehavior">
|
||||||
|
|
||||||
<!-- minHeight="0dp" is important to collapse on scroll -->
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
android:id="@+id/spaceExploreCollapsingToolbarLayout"
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="match_parent"
|
||||||
android:minHeight="0dp"
|
app:contentScrim="?android:colorBackground"
|
||||||
app:title="@string/pick_tings_to_leave"
|
app:layout_scrollFlags="scroll|exitUntilCollapsed|enterAlways|snap"
|
||||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"/>
|
app:scrimAnimationDuration="250"
|
||||||
|
app:scrimVisibleHeightTrigger="120dp"
|
||||||
|
app:titleEnabled="false"
|
||||||
|
app:toolbarId="@id/toolbar">
|
||||||
|
|
||||||
<androidx.appcompat.widget.SearchView
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
android:id="@+id/publicRoomsFilter"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginTop="?attr/actionBarSize"
|
||||||
android:layout_marginTop="8dp"
|
android:minHeight="0dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:orientation="vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingHorizontal="16dp">
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/addRoomToSpaceToolbar"
|
<TextView
|
||||||
app:queryHint="@string/search_hint_room_name" />
|
android:id="@+id/spaceLeavePromptTitle"
|
||||||
|
style="@style/TextAppearance.Vector.Body.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/space_leave_prompt_msg_with_name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/spaceLeavePromptDescription"
|
||||||
|
style="@style/TextAppearance.Vector.Body"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/space_leave_prompt_msg_only_you"
|
||||||
|
android:textColor="?vctr_content_secondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/spaceLeaveRadioButtonsTitle"
|
||||||
|
style="@style/TextAppearance.Vector.Subtitle.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/space_leave_radio_buttons_title"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textColor="?vctr_content_primary" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/spaceLeaveSelectGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:weightSum="2">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatRadioButton
|
||||||
|
android:id="@+id/spaceLeaveSelectAll"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/space_leave_radio_button_all" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatRadioButton
|
||||||
|
android:id="@+id/spaceLeaveSelectNone"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/space_leave_radio_button_none" />
|
||||||
|
|
||||||
|
<!-- This view should never be visible! There are three possible states but only two buttons by design.-->
|
||||||
|
<!-- Third button is needed to make radiogroup work as expected, it's selected, but never shown-->
|
||||||
|
<androidx.appcompat.widget.AppCompatRadioButton
|
||||||
|
android:id="@+id/spaceLeaveSelectSemi"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:layout_collapseMode="pin"
|
||||||
|
app:title="Leave space" />
|
||||||
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/spacePreviewButtonBar"
|
|
||||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:background="?vctr_system"
|
android:background="@color/palette_white"
|
||||||
android:elevation="2dp"
|
android:elevation="2dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="8dp">
|
android:padding="8dp"
|
||||||
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/spaceLeaveCancel"
|
android:id="@+id/spaceLeaveCancel"
|
||||||
@ -68,7 +134,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/leave_space" />
|
android:text="@string/leave_space" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -136,8 +136,8 @@
|
|||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:background="?vctr_list_separator_system"
|
android:background="?vctr_list_separator_system"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="@id/joinSuggestedRoomButton"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="@id/roomNameView"
|
||||||
app:layout_constraintTop_toBottomOf="@id/inlineErrorText" />
|
app:layout_constraintTop_toBottomOf="@id/inlineErrorText" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/space_explore_rooms_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/space_explore_filter_no_result_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/space_explore_filter_no_result_description"
|
||||||
|
android:textColor="?vctr_content_secondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/create_new_room"
|
||||||
|
android:textColor="?colorSecondary" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<!-- Size will be overrode -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locationLiveInactiveMap"
|
||||||
|
android:layout_width="300dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:contentDescription="@string/a11y_static_map_image"
|
||||||
|
android:src="@drawable/bg_no_location_map"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locationLiveInactiveBanner"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:alpha="0.75"
|
||||||
|
android:src="?android:colorBackground"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/locationLiveInactiveMap"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/locationLiveInactiveMap"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/locationLiveInactiveMap"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locationLiveInactiveIcon"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="65dp"
|
||||||
|
android:src="@drawable/ic_attachment_location_white"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/locationLiveInactiveVerticalCenter"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/locationLiveInactiveMap"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/locationLiveInactiveMap"
|
||||||
|
app:tint="?vctr_content_quaternary"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/locationLiveInactiveBannerIcon"
|
||||||
|
android:layout_width="26dp"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@drawable/circle"
|
||||||
|
android:backgroundTint="?vctr_content_quaternary"
|
||||||
|
android:padding="3dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/locationLiveInactiveBanner"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/locationLiveInactiveBanner"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/locationLiveInactiveBanner"
|
||||||
|
app:srcCompat="@drawable/ic_attachment_location_live_white"
|
||||||
|
app:tint="?android:colorBackground"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/locationLiveInactiveTitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:text="@string/location_share_live_ended"
|
||||||
|
android:textColor="?vctr_content_tertiary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/locationLiveInactiveBanner"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/locationLiveInactiveBannerIcon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/locationLiveInactiveBanner" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/locationLiveInactiveVerticalCenter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_percent="0.5" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -19,8 +19,8 @@
|
|||||||
android:id="@+id/locationLiveStartBanner"
|
android:id="@+id/locationLiveStartBanner"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:alpha="0.85"
|
android:alpha="0.75"
|
||||||
android:src="?colorSurface"
|
android:src="?android:colorBackground"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartMap"
|
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartMap"
|
||||||
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
|
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
|
||||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
|
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
|
||||||
@ -28,9 +28,10 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/locationLiveStartIcon"
|
android:id="@+id/locationLiveStartIcon"
|
||||||
android:layout_width="32dp"
|
android:layout_width="26dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="26dp"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginVertical="8dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
android:background="@drawable/circle"
|
android:background="@drawable/circle"
|
||||||
android:backgroundTint="?vctr_content_quaternary"
|
android:backgroundTint="?vctr_content_quaternary"
|
||||||
android:padding="3dp"
|
android:padding="3dp"
|
||||||
@ -38,6 +39,7 @@
|
|||||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartBanner"
|
app:layout_constraintStart_toStartOf="@id/locationLiveStartBanner"
|
||||||
app:layout_constraintTop_toTopOf="@id/locationLiveStartBanner"
|
app:layout_constraintTop_toTopOf="@id/locationLiveStartBanner"
|
||||||
app:srcCompat="@drawable/ic_attachment_location_live_white"
|
app:srcCompat="@drawable/ic_attachment_location_live_white"
|
||||||
|
app:tint="?android:colorBackground"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|