Merge branch 'develop' into dependency-cleanup
4
.github/workflows/post-pr.yml
vendored
|
@ -325,5 +325,5 @@ jobs:
|
|||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
hookshot_url: ${{ secrets.ELEMENT_ANDROID_HOOKSHOT_URL }}
|
||||
text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}<br />{{icon conclusion}} {{name}} <font color='{{color conclusion}}'>{{conclusion}} at {{completed_at}} <a href=\"{{html_url}}\">[details]</a></font>{{/if}}{{/with}}{{/each}}"
|
||||
text_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }} {{name}} {{conclusion}} at {{completed_at}}, {{/if}}{{/with}}{{/each}}"
|
||||
html_template: "Post-merge validation of ${{ github.head_ref }} into ${{ github.base_ref }} by ${{ github.event.pull_request.merged_by.login }} failed: {{#each job_statuses }}{{#with this }}{{#if completed }}<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',
|
||||
'jp.wasabeef',
|
||||
'junit',
|
||||
'me.leolin',
|
||||
'me.saket',
|
||||
'net.bytebuddy',
|
||||
'net.java',
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.lib.core.utils.flow
|
||||
|
||||
import android.os.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -68,10 +69,10 @@ fun <T> Flow<T>.chunk(durationInMillis: Long): Flow<List<T>> {
|
|||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
|
||||
var windowStartTime = System.currentTimeMillis()
|
||||
var windowStartTime = SystemClock.elapsedRealtime()
|
||||
var emitted = false
|
||||
collect { value ->
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val delta = currentTime - windowStartTime
|
||||
if (delta >= windowDuration) {
|
||||
windowStartTime += delta / windowDuration * windowDuration
|
||||
|
|
|
@ -18,11 +18,11 @@ package org.billcarsonfr.jsonviewer
|
|||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.ContextMenu
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.getSystemService
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyHolder
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
|
@ -77,8 +77,7 @@ internal abstract class ValueItem : EpoxyModelWithHolder<ValueItem.Holder>() {
|
|||
) {
|
||||
if (copyValue != null) {
|
||||
val menuItem = menu?.add(R.string.copy_value)
|
||||
val clipService =
|
||||
v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
val clipService = v?.context?.getSystemService<ClipboardManager>()
|
||||
menuItem?.setOnMenuItemClickListener {
|
||||
clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
|
||||
true
|
||||
|
|
|
@ -2,10 +2,20 @@
|
|||
<resources>
|
||||
|
||||
<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:padding">0dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</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>
|
||||
|
|
|
@ -27,11 +27,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
|
|||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.coroutines.Continuation
|
||||
|
@ -252,4 +254,48 @@ class VerificationTest : InstrumentedTest {
|
|||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
|
||||
val defaultSessionParams = SessionTestParams(true)
|
||||
val testHelper = CommonTestHelper(context())
|
||||
|
||||
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
|
||||
val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
|
||||
|
||||
val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
|
||||
|
||||
val serviceOfVerified = aliceSessionToVerify.cryptoService().verificationService()
|
||||
val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService()
|
||||
val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService()
|
||||
|
||||
serviceOfVerifier.addListener(object : VerificationService.Listener {
|
||||
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||
// Accept verification request
|
||||
serviceOfVerifier.readyPendingVerification(
|
||||
verificationMethods,
|
||||
pr.otherUserId,
|
||||
pr.transactionId!!,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
serviceOfVerified.requestKeyVerification(
|
||||
methods = verificationMethods,
|
||||
otherUserId = aliceSessionToVerify.myUserId,
|
||||
otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
|
||||
)
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
|
||||
requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSessionToVerify)
|
||||
testHelper.signOutAndClose(aliceSessionThatVerifies)
|
||||
testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,9 +140,24 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
|
||||
assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
|
||||
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||
// 6 + 1 + 50
|
||||
assertEquals(57, snapshot.size)
|
||||
|
||||
// We explicitly test all the types we expect here, as we expect 51 messages and "some" state events
|
||||
// But state events can change over time. So this acts as a kinda documentation of what we expect and
|
||||
// provides a good error message if it doesn't match
|
||||
|
||||
val snapshotTypes = mutableMapOf<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
|
||||
|
@ -152,8 +167,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
val snapshot = runBlocking {
|
||||
aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
|
||||
}
|
||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 6 + numberOfMessagesToSend &&
|
||||
// 7 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 7 + numberOfMessagesToSend &&
|
||||
snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||
|
||||
// The timeline is fully loaded
|
||||
|
|
|
@ -74,8 +74,12 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
|
||||
snapshot.size == 8
|
||||
// Ok, we have the 9 first messages of the initial sync (room creation and bob invite and join events)
|
||||
// create
|
||||
// join alice
|
||||
// power_levels, join_rules, history_visibility, guest_access, name
|
||||
// invite, join bob
|
||||
snapshot.size == 9
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
@ -192,7 +196,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
snapshot.size == 44 // 8 + 1 + 35
|
||||
snapshot.size == 45 // 9 + 1 + 35
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
@ -220,8 +224,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
|
|||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||
// 8 for room creation item 60 message from Alice
|
||||
snapshot.size == 68 && // 8 + 60
|
||||
// 9 for room creation item 60 message from Alice
|
||||
snapshot.size == 69 && // 9 + 60U
|
||||
snapshot.checkSendOrder(secondMessage, 30, 0) &&
|
||||
snapshot.checkSendOrder(firstMessage, 30, 30)
|
||||
}
|
||||
|
|
|
@ -177,7 +177,7 @@ object MatrixPatterns {
|
|||
* - "@alice:domain.org".getDomain() will return "domain.org"
|
||||
* - "@bob:domain.org:3455".getDomain() will return "domain.org:3455"
|
||||
*/
|
||||
fun String.getDomain(): String {
|
||||
fun String.getServerName(): String {
|
||||
if (BuildConfig.DEBUG && !isUserId(this)) {
|
||||
// They are some invalid userId localpart in the wild, but the domain part should be there anyway
|
||||
Timber.w("Not a valid user ID: $this")
|
||||
|
|
|
@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) {
|
|||
MismatchedKeys("m.key_mismatch", "Key mismatch"),
|
||||
UserError("m.user_error", "User error"),
|
||||
MismatchedUser("m.user_mismatch", "User mismatch"),
|
||||
QrCodeInvalid("m.qr_code.invalid", "Invalid QR code")
|
||||
QrCodeInvalid("m.qr_code.invalid", "Invalid QR code"),
|
||||
AcceptedByAnotherDevice("m.accepted", "Verification request accepted by another device")
|
||||
}
|
||||
|
||||
fun safeValueOf(code: String?): CancelCode {
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
|
@ -375,11 +376,11 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
|||
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||
} else {
|
||||
content.toModel<MessageContent>()?.relatesTo ?: run {
|
||||
// Special case to handle stickers, while there is only a local msgtype for stickers
|
||||
if (getClearType() == EventType.STICKER) {
|
||||
getClearContent().toModel<MessageStickerContent>()?.relatesTo
|
||||
} else {
|
||||
null
|
||||
// Special cases when there is only a local msgtype for some event types
|
||||
when (getClearType()) {
|
||||
EventType.STICKER -> getClearContent().toModel<MessageStickerContent>()?.relatesTo
|
||||
in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel<MessageBeaconLocationDataContent>()?.relatesTo
|
||||
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.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
|
@ -140,6 +141,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
|||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
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.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import android.net.Uri
|
|||
import dagger.Lazy
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -381,7 +381,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||
|
||||
return getWellknownTask.execute(
|
||||
GetWellknownTask.Params(
|
||||
domain = matrixId.getDomain(),
|
||||
domain = matrixId.getServerName().substringBeforeLast(":"),
|
||||
homeServerConnectionConfig = homeServerConnectionConfig.orWellKnownDefaults()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -942,6 +942,22 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
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? {
|
||||
|
|
|
@ -49,6 +49,11 @@ internal interface VerificationTransport {
|
|||
otherUserDeviceId: String?,
|
||||
code: CancelCode)
|
||||
|
||||
fun cancelTransaction(transactionId: String,
|
||||
otherUserId: String,
|
||||
otherUserDeviceIds: List<String>,
|
||||
code: CancelCode)
|
||||
|
||||
fun done(transactionId: String,
|
||||
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,
|
||||
onDone: (() -> Unit)?) {
|
||||
Timber.d("## SAS sending done for $transactionId")
|
||||
|
|
|
@ -193,6 +193,27 @@ internal class VerificationTransportToDevice(
|
|||
.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,
|
||||
keyAgreementProtocol: 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.permalinks.DefaultPermalinkService
|
||||
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DefaultLiveLocationAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
|
@ -387,7 +385,4 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
|
||||
|
||||
@Binds
|
||||
abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.session.homeserver
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
@ -93,10 +93,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||
}
|
||||
}.getOrNull()
|
||||
|
||||
// Domain may include a port (eg, matrix.org:8080)
|
||||
// Per https://spec.matrix.org/latest/client-server-api/#well-known-uri we should extract the hostname from the server name
|
||||
// So we take everything before the last : as the domain for the well-known task.
|
||||
// NB: This is not always the same endpoint as capabilities / mediaConfig uses.
|
||||
val wellknownResult = runCatching {
|
||||
getWellknownTask.execute(
|
||||
GetWellknownTask.Params(
|
||||
domain = userId.getDomain(),
|
||||
domain = userId.getServerName().substringBeforeLast(":"),
|
||||
homeServerConnectionConfig = homeServerConnectionConfig
|
||||
)
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.permalinks
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -55,9 +55,9 @@ internal class ViaParameterFinder @Inject constructor(
|
|||
}
|
||||
|
||||
fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {
|
||||
val userHomeserver = userId.getDomain()
|
||||
val userHomeserver = userId.getServerName()
|
||||
return getUserIdsOfJoinedMembers(roomId)
|
||||
.map { it.getDomain() }
|
||||
.map { it.getServerName() }
|
||||
.groupBy { it }
|
||||
.mapValues { it.value.size }
|
||||
.toMutableMap()
|
||||
|
@ -92,7 +92,7 @@ internal class ViaParameterFinder @Inject constructor(
|
|||
.orEmpty()
|
||||
.toSet()
|
||||
|
||||
return userThatCanInvite.map { it.getDomain() }
|
||||
return userThatCanInvite.map { it.getServerName() }
|
||||
.groupBy { it }
|
||||
.mapValues { it.value.size }
|
||||
.toMutableMap()
|
||||
|
|
|
@ -193,9 +193,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
in EventType.BEACON_LOCATION_DATA -> {
|
||||
event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let {
|
||||
liveLocationAggregationProcessor.handleBeaconLocationData(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
|
||||
|
@ -260,6 +258,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
in EventType.BEACON_LOCATION_DATA -> {
|
||||
handleBeaconLocationData(event, realm, roomId, isLocalEcho)
|
||||
}
|
||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
|
@ -756,4 +757,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
verifSummary.sourceEvents.add(event.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
|
||||
event.getClearContent().toModel<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
|
||||
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.extensions.orTrue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface LiveLocationAggregationProcessor {
|
||||
fun handleBeaconInfo(
|
||||
realm: Realm,
|
||||
event: Event,
|
||||
content: MessageBeaconInfoContent,
|
||||
roomId: String,
|
||||
isLocalEcho: Boolean,
|
||||
internal class LiveLocationAggregationProcessor @Inject constructor() {
|
||||
|
||||
fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
|
||||
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
|
||||
return
|
||||
}
|
||||
|
||||
val targetEventId = if (content.isLive.orTrue()) {
|
||||
event.eventId
|
||||
} else {
|
||||
// when live is set to false, we use the id of the event that should have been replaced
|
||||
event.unsignedData?.replacesState
|
||||
}
|
||||
|
||||
if (targetEventId.isNullOrEmpty()) {
|
||||
Timber.w("no target event id found for the beacon content")
|
||||
return
|
||||
}
|
||||
|
||||
val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
eventId = targetEventId
|
||||
)
|
||||
|
||||
Timber.d("updating summary of id=$targetEventId with isLive=${content.isLive}")
|
||||
|
||||
aggregatedSummary.endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
||||
aggregatedSummary.isActive = content.isLive
|
||||
}
|
||||
|
||||
fun handleBeaconLocationData(
|
||||
realm: Realm,
|
||||
event: Event,
|
||||
content: MessageBeaconLocationDataContent,
|
||||
roomId: String,
|
||||
isLocalEcho: Boolean,
|
||||
relatedEventId: String?,
|
||||
isLocalEcho: Boolean
|
||||
) {
|
||||
if (event.senderId.isNullOrEmpty() || isLocalEcho) {
|
||||
return
|
||||
}
|
||||
|
||||
if (relatedEventId.isNullOrEmpty()) {
|
||||
Timber.w("no related event id found for the live location content")
|
||||
return
|
||||
}
|
||||
|
||||
val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
eventId = relatedEventId
|
||||
)
|
||||
val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
|
||||
val currentLocationTimestamp = ContentMapper
|
||||
.map(aggregatedSummary.lastLocationContent)
|
||||
.toModel<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
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
|
@ -65,6 +65,6 @@ internal class RoomAliasAvailabilityChecker @Inject constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getDomain()
|
||||
internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.getServerName()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,9 @@ echo "Search for forbidden patterns in code..."
|
|||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt \
|
||||
./matrix-sdk-android/src/main/java \
|
||||
./matrix-sdk-android-flow/src/main/java \
|
||||
./library/core-utils/src/main/java \
|
||||
./library/jsonviewer/src/main/java \
|
||||
./library/ui-styles/src/main/java \
|
||||
./vector/src/main/java \
|
||||
./vector/src/debug/java \
|
||||
./vector/src/release/java \
|
||||
|
@ -100,6 +103,7 @@ echo
|
|||
echo "Search for forbidden patterns in resources..."
|
||||
|
||||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt \
|
||||
./library/ui-styles/src/main/res/values \
|
||||
./vector/src/main/res/color \
|
||||
./vector/src/main/res/layout \
|
||||
./vector/src/main/res/values \
|
||||
|
|
|
@ -450,9 +450,6 @@ dependencies {
|
|||
kapt libs.github.glideCompiler
|
||||
implementation 'com.github.yalantis:ucrop:2.2.8'
|
||||
|
||||
// Badge for compatibility
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
|
||||
// Chat effects
|
||||
implementation 'nl.dionsegijn:konfetti-xml:2.0.2'
|
||||
|
||||
|
|
|
@ -104,11 +104,10 @@ class SpaceMenuRobot {
|
|||
|
||||
fun leaveSpace() {
|
||||
clickOnSheet(R.id.leaveSpace)
|
||||
waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
|
||||
clickOn(R.id.leave_selected)
|
||||
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||
}
|
||||
clickOn(R.id.spaceLeaveSelectAll)
|
||||
clickOn(R.id.spaceLeaveButton)
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
@ -32,7 +29,6 @@ import im.vector.app.BuildConfig
|
|||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.network.WifiDetector
|
||||
import im.vector.app.core.pushers.PushersManager
|
||||
import im.vector.app.features.badge.BadgeProxy
|
||||
import im.vector.app.features.notifications.NotifiableEventResolver
|
||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
|
@ -152,10 +148,6 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
|||
Timber.tag(loggerTag.value).d("## onMessageReceivedInternal()")
|
||||
}
|
||||
|
||||
// update the badge counter
|
||||
val unreadCount = data["unread"]?.let { Integer.parseInt(it) } ?: 0
|
||||
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)
|
||||
|
||||
val session = activeSessionHolder.getSafeActiveSession()
|
||||
|
||||
if (session == null) {
|
||||
|
|
|
@ -369,11 +369,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
<br/>
|
||||
Copyright 2012 Square, Inc.
|
||||
</li>
|
||||
<li>
|
||||
<b>ShortcutBadger</b>
|
||||
<br/>
|
||||
Copyright 2014 Leo Lin
|
||||
</li>
|
||||
<li>
|
||||
<b>diff-match-patch</b>
|
||||
<br/>
|
||||
|
|
|
@ -55,6 +55,7 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||
import im.vector.app.core.di.ActivityEntryPoint
|
||||
import im.vector.app.core.dialogs.DialogLocker
|
||||
import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
||||
import im.vector.app.core.error.fatalError
|
||||
import im.vector.app.core.extensions.observeEvent
|
||||
import im.vector.app.core.extensions.observeNotNull
|
||||
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||
|
@ -611,11 +612,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||
}
|
||||
}.show()
|
||||
} else {
|
||||
if (vectorPreferences.failFast()) {
|
||||
error("No CoordinatorLayout to display this snackbar!")
|
||||
} else {
|
||||
Timber.w("No CoordinatorLayout to display this snackbar!")
|
||||
}
|
||||
fatalError("No CoordinatorLayout to display this snackbar!", vectorPreferences.failFast())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,27 +19,30 @@ package im.vector.app.core.resources
|
|||
import org.threeten.bp.Instant
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import org.threeten.bp.ZoneId
|
||||
import org.threeten.bp.ZoneOffset
|
||||
|
||||
object DateProvider {
|
||||
|
||||
private val zoneId = ZoneId.systemDefault()
|
||||
private val zoneOffset by lazy {
|
||||
val now = currentLocalDateTime()
|
||||
zoneId.rules.getOffset(now)
|
||||
}
|
||||
// recompute the zoneId each time we access it to handle change of timezones
|
||||
private val defaultZoneId: ZoneId
|
||||
get() = ZoneId.systemDefault()
|
||||
|
||||
// recompute the zoneOffset each time we access it to handle change of timezones
|
||||
private val defaultZoneOffset: ZoneOffset
|
||||
get() = defaultZoneId.rules.getOffset(currentLocalDateTime())
|
||||
|
||||
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
|
||||
val instant = Instant.ofEpochMilli(timestamp ?: 0)
|
||||
return LocalDateTime.ofInstant(instant, zoneId)
|
||||
return LocalDateTime.ofInstant(instant, defaultZoneId)
|
||||
}
|
||||
|
||||
fun currentLocalDateTime(): LocalDateTime {
|
||||
val instant = Instant.now()
|
||||
return LocalDateTime.ofInstant(instant, zoneId)
|
||||
return LocalDateTime.ofInstant(instant, defaultZoneId)
|
||||
}
|
||||
|
||||
fun toTimestamp(localDateTime: LocalDateTime): Long {
|
||||
return localDateTime.toInstant(zoneOffset).toEpochMilli()
|
||||
return localDateTime.toInstant(defaultZoneOffset).toEpochMilli()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,15 @@ package im.vector.app.core.utils
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import im.vector.app.R
|
||||
import org.threeten.bp.Duration
|
||||
import java.util.TreeMap
|
||||
|
||||
object TextUtils {
|
||||
|
||||
private const val MINUTES_PER_HOUR = 60
|
||||
private const val SECONDS_PER_MINUTE = 60
|
||||
|
||||
private val suffixes = TreeMap<Int, String>().also {
|
||||
it[1000] = "k"
|
||||
it[1000000] = "M"
|
||||
|
@ -71,13 +75,63 @@ object TextUtils {
|
|||
}
|
||||
|
||||
fun formatDuration(duration: Duration): String {
|
||||
val hours = duration.seconds / 3600
|
||||
val minutes = (duration.seconds % 3600) / 60
|
||||
val seconds = duration.seconds % 60
|
||||
val hours = getHours(duration)
|
||||
val minutes = getMinutes(duration)
|
||||
val seconds = getSeconds(duration)
|
||||
return if (hours > 0) {
|
||||
String.format("%d:%02d:%02d", hours, minutes, seconds)
|
||||
} else {
|
||||
String.format("%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatDurationWithUnits(context: Context, duration: Duration): String {
|
||||
val hours = getHours(duration)
|
||||
val minutes = getMinutes(duration)
|
||||
val seconds = getSeconds(duration)
|
||||
val builder = StringBuilder()
|
||||
when {
|
||||
hours > 0 -> {
|
||||
appendHours(context, builder, hours)
|
||||
if (minutes > 0) {
|
||||
builder.append(" ")
|
||||
appendMinutes(context, builder, minutes)
|
||||
}
|
||||
if (seconds > 0) {
|
||||
builder.append(" ")
|
||||
appendSeconds(context, builder, seconds)
|
||||
}
|
||||
}
|
||||
minutes > 0 -> {
|
||||
appendMinutes(context, builder, minutes)
|
||||
if (seconds > 0) {
|
||||
builder.append(" ")
|
||||
appendSeconds(context, builder, seconds)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
appendSeconds(context, builder, seconds)
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun appendHours(context: Context, builder: StringBuilder, hours: Int) {
|
||||
builder.append(hours)
|
||||
builder.append(context.resources.getString(R.string.time_unit_hour_short))
|
||||
}
|
||||
|
||||
private fun appendMinutes(context: Context, builder: StringBuilder, minutes: Int) {
|
||||
builder.append(minutes)
|
||||
builder.append(context.getString(R.string.time_unit_minute_short))
|
||||
}
|
||||
|
||||
private fun appendSeconds(context: Context, builder: StringBuilder, seconds: Int) {
|
||||
builder.append(seconds)
|
||||
builder.append(context.getString(R.string.time_unit_second_short))
|
||||
}
|
||||
|
||||
private fun getHours(duration: Duration): Int = duration.toHours().toInt()
|
||||
private fun getMinutes(duration: Duration): Int = duration.toMinutes().toInt() % MINUTES_PER_HOUR
|
||||
private fun getSeconds(duration: Duration): Int = (duration.seconds % SECONDS_PER_MINUTE).toInt()
|
||||
}
|
||||
|
|
|
@ -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 vectorPreferences: VectorPreferences,
|
||||
private val urlMapProvider: UrlMapProvider,
|
||||
private val liveLocationMessageItemFactory: LiveLocationMessageItemFactory,
|
||||
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
||||
) {
|
||||
|
||||
// TODO inject this properly?
|
||||
|
@ -216,7 +216,7 @@ class MessageItemFactory @Inject constructor(
|
|||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
is MessageBeaconInfoContent -> liveLocationMessageItemFactory.create(messageContent, highlight, attributes)
|
||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
return messageItem?.apply {
|
||||
|
@ -237,14 +237,14 @@ class MessageItemFactory @Inject constructor(
|
|||
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
||||
}
|
||||
|
||||
val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
|
||||
val locationUserId = if (locationContent.isSelfLocation()) informationData.senderId else null
|
||||
|
||||
return MessageLocationItem_()
|
||||
.attributes(attributes)
|
||||
.locationUrl(locationUrl)
|
||||
.mapWidth(width)
|
||||
.mapHeight(height)
|
||||
.userId(userId)
|
||||
.locationUserId(locationUserId)
|
||||
.locationPinProvider(locationPinProvider)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
|
|
@ -113,7 +113,8 @@ class TimelineItemFactory @Inject constructor(
|
|||
EventType.CALL_NEGOTIATE,
|
||||
EventType.REACTION,
|
||||
in EventType.POLL_RESPONSE,
|
||||
in EventType.POLL_END -> noticeItemFactory.create(params)
|
||||
in EventType.POLL_END,
|
||||
in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params)
|
||||
// Calls
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
|
|
|
@ -107,7 +107,8 @@ class NoticeEventFormatter @Inject constructor(
|
|||
EventType.REDACTION,
|
||||
EventType.STICKER,
|
||||
in EventType.POLL_RESPONSE,
|
||||
in EventType.POLL_END -> formatDebug(timelineEvent.root)
|
||||
in EventType.POLL_END,
|
||||
in EventType.BEACON_LOCATION_DATA -> formatDebug(timelineEvent.root)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
|
|
|
@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
|
|||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline.
|
||||
* TODO Update this comment
|
||||
* This class is responsible of building extra information data associated to a given event.
|
||||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
|
@ -119,7 +118,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
isFirstFromThisSender = isFirstFromThisSender,
|
||||
isLastFromThisSender = isLastFromThisSender,
|
||||
e2eDecoration = e2eDecoration,
|
||||
sendStateDecoration = sendStateDecoration
|
||||
sendStateDecoration = sendStateDecoration,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ class MessageItemAttributesFactory @Inject constructor(
|
|||
memberClickListener = {
|
||||
callback?.onMemberNameClicked(informationData)
|
||||
},
|
||||
callback = callback,
|
||||
reactionPillCallback = callback,
|
||||
avatarCallback = callback,
|
||||
threadCallback = callback,
|
||||
|
|
|
@ -178,6 +178,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
override val itemLongClickListener: View.OnLongClickListener? = null,
|
||||
override val itemClickListener: ClickListener? = null,
|
||||
val memberClickListener: ClickListener? = null,
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
||||
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
||||
val threadCallback: TimelineEventController.ThreadCallback? = null,
|
||||
|
|
|
@ -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 sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
|
||||
val isFirstFromThisSender: Boolean = false,
|
||||
val isLastFromThisSender: Boolean = false
|
||||
val isLastFromThisSender: Boolean = false,
|
||||
) : Parcelable {
|
||||
|
||||
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
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageLiveLocationStartItem : AbsMessageItem<MessageLiveLocationStartItem.Holder>() {
|
||||
abstract class MessageLiveLocationStartItem :
|
||||
AbsMessageItem<MessageLiveLocationStartItem.Holder>(),
|
||||
LiveLocationShareStatusItem by DefaultLiveLocationShareStatusItem() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var mapWidth: Int = 0
|
||||
|
@ -42,44 +35,8 @@ abstract class MessageLiveLocationStartItem : AbsMessageItem<MessageLiveLocation
|
|||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.view, null)
|
||||
bindMap(holder)
|
||||
bindBottomBanner(holder)
|
||||
}
|
||||
|
||||
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)
|
||||
bindMap(holder.noLocationMapImageView, mapWidth, mapHeight, attributes.informationData.messageLayout)
|
||||
bindBottomBanner(holder.bannerImageView, attributes.informationData.messageLayout)
|
||||
}
|
||||
|
||||
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");
|
||||
* 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
|
||||
|
||||
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.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
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageLocationItem : AbsMessageItem<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)
|
||||
}
|
||||
abstract class MessageLocationItem : AbsMessageLocationItem<MessageLocationItem.Holder>() {
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.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)
|
||||
}
|
||||
class Holder : AbsMessageLocationItem.Holder(STUB_ID)
|
||||
|
||||
companion object {
|
||||
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_BEACON_INFO,
|
||||
)
|
||||
|
||||
private val MSG_TYPES_WITH_LOCATION_DATA = setOf(
|
||||
MessageType.MSGTYPE_LOCATION,
|
||||
MessageType.MSGTYPE_BEACON_LOCATION_DATA
|
||||
)
|
||||
}
|
||||
|
||||
private val cornerRadius: Float by lazy {
|
||||
|
@ -145,9 +150,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
}
|
||||
|
||||
private fun MessageContent?.timestampInsideMessage(): Boolean {
|
||||
if (this == null) return false
|
||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
||||
return this.msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
|
||||
return when {
|
||||
this == null -> false
|
||||
msgType in MSG_TYPES_WITH_LOCATION_DATA -> vectorPreferences.labsRenderLocationsInTimeline()
|
||||
else -> msgType in MSG_TYPES_WITH_TIMESTAMP_INSIDE_MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageContent?.shouldAddMessageOverlay(): Boolean {
|
||||
|
|
|
@ -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_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
|
||||
|
|
|
@ -29,7 +29,7 @@ data class LocationData(
|
|||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Creates location data from a LocationContent.
|
||||
* Creates location data from a MessageLocationContent.
|
||||
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||
* @return location data or null if geo uri is not valid
|
||||
*/
|
||||
|
@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? {
|
|||
return parseGeo(getBestGeoUri())
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates location data from a geoUri String.
|
||||
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||
* @return location data or null if geo uri is null or not valid
|
||||
*/
|
||||
fun String?.toLocationData(): LocationData? {
|
||||
return this?.let { parseGeo(it) }
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun parseGeo(geo: String): LocationData? {
|
||||
val geoParts = geo
|
||||
|
|
|
@ -55,7 +55,10 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
|
||||
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>()
|
||||
|
||||
override fun onCreate() {
|
||||
|
@ -73,8 +76,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
||||
|
||||
if (roomArgs != null) {
|
||||
roomArgsList.add(roomArgs)
|
||||
|
||||
// Show a sticky notification
|
||||
val notification = notificationUtils.buildLiveLocationSharingNotification()
|
||||
startForeground(roomArgs.roomId.hashCode(), notification)
|
||||
|
@ -87,7 +88,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
.getSafeActiveSession()
|
||||
?.let { session ->
|
||||
session.coroutineScope.launch(session.coroutineDispatchers.io) {
|
||||
sendLiveBeaconInfo(session, roomArgs)
|
||||
sendStartingLiveBeaconInfo(session, roomArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +96,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
return START_STICKY
|
||||
}
|
||||
|
||||
private suspend fun sendLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
|
||||
private suspend fun sendStartingLiveBeaconInfo(session: Session, roomArgs: RoomArgs) {
|
||||
val beaconContent = MessageBeaconInfoContent(
|
||||
timeout = roomArgs.durationMillis,
|
||||
isLive = true,
|
||||
|
@ -103,7 +104,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
).toContent()
|
||||
|
||||
val stateKey = session.myUserId
|
||||
session
|
||||
val beaconEventId = session
|
||||
.getRoom(roomArgs.roomId)
|
||||
?.stateService()
|
||||
?.sendStateEvent(
|
||||
|
@ -111,6 +112,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
stateKey = stateKey,
|
||||
body = beaconContent
|
||||
)
|
||||
|
||||
beaconEventId
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?.let {
|
||||
roomArgsMap[it] = roomArgs
|
||||
locationTracker.requestLastKnownLocation()
|
||||
}
|
||||
?: run {
|
||||
Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id")
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleTimer(roomId: String, durationMillis: Long) {
|
||||
|
@ -134,9 +145,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
// Send a new beacon info state by setting live field as false
|
||||
sendStoppedBeaconInfo(roomId)
|
||||
|
||||
synchronized(roomArgsList) {
|
||||
roomArgsList.removeAll { it.roomId == roomId }
|
||||
if (roomArgsList.isEmpty()) {
|
||||
synchronized(roomArgsMap) {
|
||||
val beaconIds = roomArgsMap
|
||||
.filter { it.value.roomId == roomId }
|
||||
.map { it.key }
|
||||
beaconIds.forEach { roomArgsMap.remove(it) }
|
||||
|
||||
if (roomArgsMap.isEmpty()) {
|
||||
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
|
||||
destroyMe()
|
||||
}
|
||||
|
@ -156,16 +171,17 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
||||
|
||||
val session = activeSessionHolder.getSafeActiveSession()
|
||||
// Emit location update to all rooms in which live location sharing is active
|
||||
session?.coroutineScope?.launch(session.coroutineDispatchers.io) {
|
||||
roomArgsList.toList().forEach { roomArg ->
|
||||
sendLiveLocation(roomArg.roomId, locationData)
|
||||
}
|
||||
roomArgsMap.toMap().forEach { item ->
|
||||
sendLiveLocation(item.value.roomId, item.key, locationData)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendLiveLocation(roomId: String, locationData: LocationData) {
|
||||
private fun sendLiveLocation(
|
||||
roomId: String,
|
||||
beaconInfoEventId: String,
|
||||
locationData: LocationData
|
||||
) {
|
||||
val session = activeSessionHolder.getSafeActiveSession()
|
||||
val room = session?.getRoom(roomId)
|
||||
val userId = session?.myUserId
|
||||
|
@ -174,19 +190,13 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
return
|
||||
}
|
||||
|
||||
room
|
||||
.stateService()
|
||||
.getLiveLocationBeaconInfo(userId, true)
|
||||
?.eventId
|
||||
?.let {
|
||||
room.sendService().sendLiveLocation(
|
||||
beaconInfoEventId = it,
|
||||
beaconInfoEventId = beaconInfoEventId,
|
||||
latitude = locationData.latitude,
|
||||
longitude = locationData.longitude,
|
||||
uncertainty = locationData.uncertainty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationProviderIsNotAvailable() {
|
||||
stopForeground(true)
|
||||
|
|
|
@ -40,10 +40,12 @@ class LocationTracker @Inject constructor(
|
|||
fun onLocationProviderIsNotAvailable()
|
||||
}
|
||||
|
||||
private var callbacks = mutableListOf<Callback>()
|
||||
private val callbacks = mutableListOf<Callback>()
|
||||
|
||||
private var hasGpsProviderLiveLocation = false
|
||||
|
||||
private var lastLocation: LocationData? = null
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
fun start() {
|
||||
Timber.d("## LocationTracker. start()")
|
||||
|
@ -92,6 +94,14 @@ class LocationTracker @Inject constructor(
|
|||
callbacks.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the last known location. It will be given async through Callback.
|
||||
* Please ensure adding a callback to receive the value.
|
||||
*/
|
||||
fun requestLastKnownLocation() {
|
||||
lastLocation?.let { location -> callbacks.forEach { it.onLocationUpdate(location) } }
|
||||
}
|
||||
|
||||
fun addCallback(callback: Callback) {
|
||||
if (!callbacks.contains(callback)) {
|
||||
callbacks.add(callback)
|
||||
|
@ -127,7 +137,9 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
callbacks.forEach { it.onLocationUpdate(location.toLocationData()) }
|
||||
val locationData = location.toLocationData()
|
||||
lastLocation = locationData
|
||||
callbacks.forEach { it.onLocationUpdate(locationData) }
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
|
|
|
@ -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 kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -607,7 +607,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
?: HomeServerConnectionConfig(
|
||||
homeServerUri = Uri.parse("https://${action.username.getDomain()}"),
|
||||
homeServerUri = Uri.parse("https://${action.username.getServerName()}"),
|
||||
homeServerUriBase = Uri.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ import im.vector.app.features.login.LoginMode
|
|||
import im.vector.app.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -640,7 +640,7 @@ class LoginViewModel2 @AssistedInject constructor(
|
|||
}
|
||||
viewEvent?.let { _viewEvents.post(it) }
|
||||
|
||||
val urlFromUser = action.username.getDomain()
|
||||
val urlFromUser = action.username.getServerName()
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
|
|
|
@ -317,6 +317,7 @@ class DefaultNavigator @Inject constructor(
|
|||
if (context is AppCompatActivity) {
|
||||
if (context !is MatrixToBottomSheet.InteractionListener) {
|
||||
fatalError("Caller context should implement MatrixToBottomSheet.InteractionListener", vectorPreferences.failFast())
|
||||
return
|
||||
}
|
||||
// TODO check if there is already one??
|
||||
MatrixToBottomSheet.withLink(link, origin)
|
||||
|
|
|
@ -20,7 +20,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.extensions.andThen
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||
|
@ -75,7 +75,7 @@ class DirectLoginUseCase @Inject constructor(
|
|||
)
|
||||
|
||||
private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
|
||||
homeServerUri = uriFactory.parse("https://${action.username.getDomain()}"),
|
||||
homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"),
|
||||
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
|
||||
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
|
||||
)
|
||||
|
|
|
@ -16,14 +16,14 @@
|
|||
|
||||
package im.vector.app.features.raw.wellknown
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
|
||||
suspend fun RawService.getElementWellknown(sessionParams: SessionParams): ElementWellKnown? {
|
||||
// By default we use the domain of the userId to retrieve the .well-known data
|
||||
val domain = sessionParams.userId.getDomain()
|
||||
val domain = sessionParams.userId.getServerName()
|
||||
return tryOrNull { getWellknown(domain) }
|
||||
?.let { ElementWellKnownMapper.from(it) }
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import im.vector.app.features.raw.wellknown.getElementWellknown
|
|||
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
|
@ -98,7 +98,7 @@ class CreateRoomViewModel @AssistedInject constructor(
|
|||
private fun initHomeServerName() {
|
||||
setState {
|
||||
copy(
|
||||
homeServerName = session.myUserId.getDomain()
|
||||
homeServerName = session.myUserId.getServerName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.resources.StringArrayProvider
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryData
|
||||
import im.vector.app.features.roomdirectory.RoomDirectoryServer
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
import javax.inject.Inject
|
||||
|
@ -37,7 +37,7 @@ class RoomDirectoryListCreator @Inject constructor(
|
|||
val protocols = ArrayList<RoomDirectoryData>()
|
||||
|
||||
// Add user homeserver name
|
||||
val userHsName = session.myUserId.getDomain()
|
||||
val userHsName = session.myUserId.getServerName()
|
||||
|
||||
// Add default protocol
|
||||
protocols.add(
|
||||
|
|
|
@ -31,7 +31,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
|
@ -96,7 +96,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo
|
|||
private fun initHomeServerName() {
|
||||
setState {
|
||||
copy(
|
||||
homeServerName = session.myUserId.getDomain()
|
||||
homeServerName = session.myUserId.getServerName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.rageshake.BugReporter
|
||||
import im.vector.app.features.roomprofile.RoomProfileActivity
|
||||
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
|
||||
import im.vector.app.features.spaces.manage.ManageType
|
||||
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
@ -109,7 +110,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
|
|||
}
|
||||
|
||||
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.launch
|
||||
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.identity.IdentityServiceListener
|
||||
import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult
|
||||
|
@ -66,7 +66,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
|
|||
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
|
||||
setState {
|
||||
copy(
|
||||
homeServerName = session.myUserId.getDomain(),
|
||||
homeServerName = session.myUserId.getServerName(),
|
||||
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.features.home.AvatarRenderer
|
||||
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 me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
@ -53,6 +55,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||
) : TypedEpoxyController<SpaceDirectoryState>() {
|
||||
|
||||
interface InteractionListener {
|
||||
fun onFilterQueryChanged(query: String?)
|
||||
fun onButtonClick(spaceChildInfo: SpaceChildInfo)
|
||||
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
|
||||
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
|
||||
|
@ -62,6 +65,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||
}
|
||||
|
||||
var listener: InteractionListener? = null
|
||||
private val matchFilter = SpaceChildInfoMatchFilter()
|
||||
|
||||
override fun buildModels(data: SpaceDirectoryState?) {
|
||||
val host = this
|
||||
|
@ -76,7 +80,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||
val failure = results.error
|
||||
if (failure is Failure.ServerError && failure.error.code == M_UNRECOGNIZED) {
|
||||
genericPillItem {
|
||||
id("HS no Support")
|
||||
id("hs_no_support")
|
||||
imageRes(R.drawable.error)
|
||||
tintIcon(false)
|
||||
text(
|
||||
|
@ -132,7 +136,15 @@ class SpaceDirectoryController @Inject constructor(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
flattenChildInfo.forEach { info ->
|
||||
matchFilter.filter = data?.currentFilter ?: ""
|
||||
val filteredChildInfo = flattenChildInfo.filter { matchFilter.test(it) }
|
||||
|
||||
if (filteredChildInfo.isEmpty()) {
|
||||
spaceDirectoryFilterNoResults {
|
||||
id("no_results")
|
||||
}
|
||||
} else {
|
||||
filteredChildInfo.forEach { info ->
|
||||
val isSpace = info.roomType == RoomType.SPACE
|
||||
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
|
||||
val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
|
||||
|
@ -172,6 +184,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hierarchySummary?.nextToken != null) {
|
||||
val paginationStatus = data.paginationStatus[currentRootId] ?: Uninitialized
|
||||
if (paginationStatus is Fail) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -44,7 +45,6 @@ import im.vector.app.core.utils.openUrlInExternalBrowser
|
|||
import im.vector.app.databinding.FragmentSpaceDirectoryBinding
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.matrixto.SpaceCardRenderer
|
||||
import im.vector.app.features.permalink.PermalinkHandler
|
||||
import im.vector.app.features.spaces.manage.ManageType
|
||||
import im.vector.app.features.spaces.manage.SpaceAddRoomSpaceChooserBottomSheet
|
||||
|
@ -63,7 +63,6 @@ data class SpaceDirectoryArgs(
|
|||
class SpaceDirectoryFragment @Inject constructor(
|
||||
private val epoxyController: SpaceDirectoryController,
|
||||
private val permalinkHandler: PermalinkHandler,
|
||||
private val spaceCardRenderer: SpaceCardRenderer,
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment<FragmentSpaceDirectoryBinding>(),
|
||||
SpaceDirectoryController.InteractionListener,
|
||||
|
@ -123,9 +122,6 @@ class SpaceDirectoryFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
views.spaceCard.matrixToCardMainButton.isVisible = false
|
||||
views.spaceCard.matrixToCardSecondaryButton.isVisible = false
|
||||
|
||||
// Hide FAB when list is scrolling
|
||||
views.spaceDirectoryList.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
|
@ -167,18 +163,37 @@ class SpaceDirectoryFragment @Inject constructor(
|
|||
// it's the root
|
||||
toolbar?.setTitle(R.string.space_explore_activity_title)
|
||||
} else {
|
||||
toolbar?.title = state.currentRootSummary?.name
|
||||
val spaceName = state.currentRootSummary?.name
|
||||
?: state.currentRootSummary?.canonicalAlias
|
||||
?: getString(R.string.space_explore_activity_title)
|
||||
|
||||
if (spaceName != null) {
|
||||
toolbar?.title = spaceName
|
||||
toolbar?.subtitle = getString(R.string.space_explore_activity_title)
|
||||
} else {
|
||||
toolbar?.title = getString(R.string.space_explore_activity_title)
|
||||
}
|
||||
}
|
||||
|
||||
spaceCardRenderer.render(state.currentRootSummary, emptyList(), this, views.spaceCard, showDescription = false)
|
||||
views.addOrCreateChatRoomButton.isVisible = state.canAddRooms
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
|
||||
menu.findItem(R.id.spaceAddRoom)?.isVisible = state.canAddRooms
|
||||
menu.findItem(R.id.spaceCreateRoom)?.isVisible = false // Not yet implemented
|
||||
|
||||
menu.findItem(R.id.spaceSearch)?.let { searchItem ->
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
onFilterQueryChanged(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
|
@ -198,6 +213,10 @@ class SpaceDirectoryFragment @Inject constructor(
|
|||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onFilterQueryChanged(query: String?) {
|
||||
viewModel.handle(SpaceDirectoryViewAction.FilterRooms(query))
|
||||
}
|
||||
|
||||
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
|
||||
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
|
|||
sealed class SpaceDirectoryViewAction : VectorViewModelAction {
|
||||
data class ExploreSubSpace(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
||||
data class JoinOrOpen(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
||||
data class FilterRooms(val query: String?) : SpaceDirectoryViewAction()
|
||||
data class ShowDetails(val spaceChildInfo: SpaceChildInfo) : SpaceDirectoryViewAction()
|
||||
data class NavigateToRoom(val roomId: String) : SpaceDirectoryViewAction()
|
||||
object CreateNewRoom : SpaceDirectoryViewAction()
|
||||
|
|
|
@ -225,8 +225,15 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToCreateNewRoom(state.currentRootSummary?.roomId ?: initialState.spaceId))
|
||||
}
|
||||
}
|
||||
is SpaceDirectoryViewAction.FilterRooms -> {
|
||||
filter(action.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filter(query: String?) {
|
||||
setState { copy(currentFilter = query.orEmpty()) }
|
||||
}
|
||||
|
||||
private fun handleBack() = withState { state ->
|
||||
if (state.hierarchyStack.isEmpty()) {
|
||||
|
|
|
@ -21,6 +21,9 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
|
||||
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
|
||||
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
|
||||
data class SetFilteringEnabled(val isEnabled: Boolean) : SpaceLeaveAdvanceViewAction()
|
||||
object DoLeave : SpaceLeaveAdvanceViewAction()
|
||||
object ClearError : SpaceLeaveAdvanceViewAction()
|
||||
object SelectAll : SpaceLeaveAdvanceViewAction()
|
||||
object SelectNone : SpaceLeaveAdvanceViewAction()
|
||||
}
|
||||
|
|
|
@ -28,8 +28,11 @@ data class SpaceLeaveAdvanceViewState(
|
|||
val allChildren: Async<List<RoomSummary>> = Uninitialized,
|
||||
val selectedRooms: List<String> = emptyList(),
|
||||
val currentFilter: String = "",
|
||||
val leaveState: Async<Unit> = Uninitialized
|
||||
val leaveState: Async<Unit> = Uninitialized,
|
||||
val isFilteringEnabled: Boolean = false,
|
||||
val isLastAdmin: Boolean = false
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: SpaceBottomSheetSettingsArgs) : this(
|
||||
spaceId = args.spaceId
|
||||
)
|
||||
|
|
|
@ -18,20 +18,23 @@ package im.vector.app.features.spaces.leave
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior
|
||||
import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import reactivecircus.flowbinding.appcompat.queryTextChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
class SpaceLeaveAdvancedFragment @Inject constructor(
|
||||
|
@ -44,11 +47,33 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||
|
||||
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
|
||||
|
||||
override fun getMenuRes() = R.menu.menu_space_leave
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar(views.toolbar)
|
||||
.allowBack()
|
||||
|
||||
controller.listener = this
|
||||
|
||||
withState(viewModel) { state ->
|
||||
setupToolbar(views.toolbar)
|
||||
.setSubtitle(state.spaceSummary?.name)
|
||||
.allowBack()
|
||||
|
||||
state.spaceSummary?.let { summary ->
|
||||
val warningMessage: CharSequence? = when {
|
||||
summary.otherMemberIds.isEmpty() -> getString(R.string.space_leave_prompt_msg_only_you)
|
||||
state.isLastAdmin -> getString(R.string.space_leave_prompt_msg_as_admin)
|
||||
!summary.isPublic -> getString(R.string.space_leave_prompt_msg_private)
|
||||
else -> null
|
||||
}
|
||||
|
||||
views.spaceLeavePromptDescription.isVisible = warningMessage != null
|
||||
views.spaceLeavePromptDescription.text = warningMessage
|
||||
}
|
||||
|
||||
views.spaceLeavePromptTitle.text = getString(R.string.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
|
||||
}
|
||||
|
||||
views.roomList.configureWith(controller)
|
||||
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
|
||||
|
||||
|
@ -56,12 +81,23 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
|
||||
}
|
||||
|
||||
views.publicRoomsFilter.queryTextChanges()
|
||||
.debounce(100)
|
||||
.onEach {
|
||||
viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString()))
|
||||
views.spaceLeaveSelectGroup.setOnCheckedChangeListener { _, optionId ->
|
||||
when (optionId) {
|
||||
R.id.spaceLeaveSelectAll -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectAll)
|
||||
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() {
|
||||
|
@ -72,10 +108,63 @@ class SpaceLeaveAdvancedFragment @Inject constructor(
|
|||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.isFilteringEnabled) {
|
||||
views.appBarLayout.setExpanded(false)
|
||||
}
|
||||
|
||||
updateAppBarBehaviorState(state)
|
||||
updateRadioButtonsState(state)
|
||||
|
||||
controller.setData(state)
|
||||
}
|
||||
|
||||
override fun onItemSelected(roomSummary: RoomSummary) {
|
||||
viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId))
|
||||
}
|
||||
|
||||
private fun updateAppBarBehaviorState(state: SpaceLeaveAdvanceViewState) {
|
||||
val behavior = (views.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior as ToggleableAppBarLayoutBehavior
|
||||
behavior.isEnabled = !state.isFilteringEnabled
|
||||
}
|
||||
|
||||
private fun updateRadioButtonsState(state: SpaceLeaveAdvanceViewState) {
|
||||
(state.allChildren as? Success)?.invoke()?.size?.let { allChildrenCount ->
|
||||
when (state.selectedRooms.size) {
|
||||
0 -> views.spaceLeaveSelectNone.isChecked = true
|
||||
allChildrenCount -> views.spaceLeaveSelectAll.isChecked = true
|
||||
else -> views.spaceLeaveSelectSemi.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MenuItem.bind(
|
||||
onExpanded: () -> Unit,
|
||||
onCollapsed: () -> Unit,
|
||||
onTextChanged: (String) -> Unit) {
|
||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
onExpanded()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
onCollapsed()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
val searchView = actionView as SearchView
|
||||
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
onTextChanged(newText ?: "")
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,9 +36,14 @@ import okhttp3.internal.toImmutableList
|
|||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.getStateEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.Role
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
import org.matrix.android.sdk.flow.unwrap
|
||||
|
@ -50,52 +55,24 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
|
|||
private val appStateHandler: AppStateHandler
|
||||
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state ->
|
||||
when (action) {
|
||||
is SpaceLeaveAdvanceViewAction.ToggleSelection -> {
|
||||
val existing = state.selectedRooms.toMutableList()
|
||||
if (existing.contains(action.roomId)) {
|
||||
existing.remove(action.roomId)
|
||||
} else {
|
||||
existing.add(action.roomId)
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
selectedRooms = existing.toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
is SpaceLeaveAdvanceViewAction.UpdateFilter -> {
|
||||
setState { copy(currentFilter = action.filter) }
|
||||
}
|
||||
SpaceLeaveAdvanceViewAction.DoLeave -> {
|
||||
setState { copy(leaveState = Loading()) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
state.selectedRooms.forEach {
|
||||
try {
|
||||
session.roomService().leaveRoom(it)
|
||||
} catch (failure: Throwable) {
|
||||
// silently ignore?
|
||||
Timber.e(failure, "Fail to leave sub rooms/spaces")
|
||||
}
|
||||
}
|
||||
|
||||
session.spaceService().leaveSpace(initialState.spaceId)
|
||||
// We observe the membership and to dismiss when we have remote echo of leaving
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(leaveState = Fail(failure)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
SpaceLeaveAdvanceViewAction.ClearError -> {
|
||||
setState { copy(leaveState = Uninitialized) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val spaceSummary = session.getRoomSummary(initialState.spaceId)
|
||||
val space = session.getRoom(initialState.spaceId)
|
||||
val spaceSummary = space?.roomSummary()
|
||||
|
||||
val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
|
||||
powerLevelsEvent?.content?.toModel<PowerLevelsContent>()?.let { powerLevelsContent ->
|
||||
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
|
||||
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
|
||||
val otherAdminCount = spaceSummary?.otherMemberIds
|
||||
?.map { powerLevelsHelper.getUserRole(it) }
|
||||
?.count { it is Role.Admin }
|
||||
?: 0
|
||||
val isLastAdmin = isAdmin && otherAdminCount == 0
|
||||
setState {
|
||||
copy(isLastAdmin = isLastAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
setState { copy(spaceSummary = spaceSummary) }
|
||||
session.getRoom(initialState.spaceId)?.let { room ->
|
||||
room.flow().liveRoomSummary()
|
||||
|
@ -127,6 +104,62 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun handle(action: SpaceLeaveAdvanceViewAction) {
|
||||
when (action) {
|
||||
is SpaceLeaveAdvanceViewAction.UpdateFilter -> setState { copy(currentFilter = action.filter) }
|
||||
SpaceLeaveAdvanceViewAction.ClearError -> setState { copy(leaveState = Uninitialized) }
|
||||
SpaceLeaveAdvanceViewAction.SelectNone -> setState { copy(selectedRooms = emptyList()) }
|
||||
is SpaceLeaveAdvanceViewAction.SetFilteringEnabled -> setState { copy(isFilteringEnabled = action.isEnabled) }
|
||||
is SpaceLeaveAdvanceViewAction.ToggleSelection -> handleSelectionToggle(action)
|
||||
SpaceLeaveAdvanceViewAction.DoLeave -> handleLeave()
|
||||
SpaceLeaveAdvanceViewAction.SelectAll -> handleSelectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSelectAll() = withState { state ->
|
||||
val filteredRooms = (state.allChildren as? Success)?.invoke()?.filter {
|
||||
it.name.contains(state.currentFilter, true)
|
||||
}
|
||||
filteredRooms?.let {
|
||||
setState { copy(selectedRooms = it.map { it.roomId }) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeave() = withState { state ->
|
||||
setState { copy(leaveState = Loading()) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
state.selectedRooms.forEach {
|
||||
try {
|
||||
session.roomService().leaveRoom(it)
|
||||
} catch (failure: Throwable) {
|
||||
// silently ignore?
|
||||
Timber.e(failure, "Fail to leave sub rooms/spaces")
|
||||
}
|
||||
}
|
||||
|
||||
session.spaceService().leaveSpace(initialState.spaceId)
|
||||
// We observe the membership and to dismiss when we have remote echo of leaving
|
||||
} catch (failure: Throwable) {
|
||||
setState { copy(leaveState = Fail(failure)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSelectionToggle(action: SpaceLeaveAdvanceViewAction.ToggleSelection) = withState { state ->
|
||||
val existing = state.selectedRooms.toMutableList()
|
||||
if (existing.contains(action.roomId)) {
|
||||
existing.remove(action.roomId)
|
||||
} else {
|
||||
existing.add(action.roomId)
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
selectedRooms = existing.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<SpaceLeaveAdvancedViewModel, SpaceLeaveAdvanceViewState> {
|
||||
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 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_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
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:contentInsetStart="0dp">
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
@ -57,7 +34,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp "
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@string/a11y_create_room"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_fab_add"
|
||||
|
|
|
@ -16,41 +16,107 @@
|
|||
tools:listitem="@layout/item_room_to_add_in_space" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="im.vector.app.core.utils.ToggleableAppBarLayoutBehavior">
|
||||
|
||||
<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|enterAlways|snap"
|
||||
app:scrimAnimationDuration="250"
|
||||
app:scrimVisibleHeightTrigger="120dp"
|
||||
app:titleEnabled="false"
|
||||
app:toolbarId="@id/toolbar">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:minHeight="0dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<TextView
|
||||
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>
|
||||
|
||||
<!-- minHeight="0dp" is important to collapse on scroll -->
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:minHeight="0dp"
|
||||
app:title="@string/pick_tings_to_leave"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"/>
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/publicRoomsFilter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/addRoomToSpaceToolbar"
|
||||
app:queryHint="@string/search_hint_room_name" />
|
||||
app:layout_collapseMode="pin"
|
||||
app:title="Leave space" />
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/spacePreviewButtonBar"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?vctr_system"
|
||||
android:background="@color/palette_white"
|
||||
android:elevation="2dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
android:padding="8dp"
|
||||
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
|
||||
|
||||
<Button
|
||||
android:id="@+id/spaceLeaveCancel"
|
||||
|
@ -68,7 +134,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/leave_space" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -136,8 +136,8 @@
|
|||
android:layout_height="1dp"
|
||||
android:background="?vctr_list_separator_system"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/joinSuggestedRoomButton"
|
||||
app:layout_constraintStart_toStartOf="@id/roomNameView"
|
||||
app:layout_constraintTop_toBottomOf="@id/inlineErrorText" />
|
||||
|
||||
</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:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:alpha="0.85"
|
||||
android:src="?colorSurface"
|
||||
android:alpha="0.75"
|
||||
android:src="?android:colorBackground"
|
||||
app:layout_constraintBottom_toBottomOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintEnd_toEndOf="@id/locationLiveStartMap"
|
||||
app:layout_constraintStart_toStartOf="@id/locationLiveStartMap"
|
||||
|
@ -28,9 +28,10 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/locationLiveStartIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
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"
|
||||
|
@ -38,6 +39,7 @@
|
|||
app:layout_constraintStart_toStartOf="@id/locationLiveStartBanner"
|
||||
app:layout_constraintTop_toTopOf="@id/locationLiveStartBanner"
|
||||
app:srcCompat="@drawable/ic_attachment_location_live_white"
|
||||
app:tint="?android:colorBackground"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
|
|