Merge branch 'develop' into feature/fga/avoid_deadlocks
This commit is contained in:
commit
cfd43af45c
307 changed files with 2806 additions and 1172 deletions
|
|
@ -59,7 +59,7 @@ fun Context.isAnimationEnabled(): Boolean {
|
|||
}
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
/**
|
||||
* Return the application label of the provided package. If not found, the package is returned.
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ fun Modifier.shapeShadow(
|
|||
val paint = Paint()
|
||||
val frameworkPaint = paint.asFrameworkPaint()
|
||||
if (blurRadius != 0.dp) {
|
||||
frameworkPaint.maskFilter = (BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL))
|
||||
frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
|
||||
}
|
||||
frameworkPaint.color = color.toArgb()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 io.element.android.libraries.designsystem.preview
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
val sheetStateForPreview = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = SheetValue.Expanded,
|
||||
)
|
||||
|
|
@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.preview.sheetStateForPreview
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -100,10 +100,7 @@ private fun ContentToPreview() {
|
|||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {},
|
||||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = SheetValue.Expanded,
|
||||
),
|
||||
sheetState = sheetStateForPreview,
|
||||
) {
|
||||
Text(
|
||||
text = "Sheet Content",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fun LogCompositions(tag: String, msg: String) {
|
|||
if (BuildConfig.DEBUG) {
|
||||
val ref = remember { Ref(0) }
|
||||
SideEffect { ref.value++ }
|
||||
Timber.d(tag, "Compositions: $msg ${ref.value}")
|
||||
Timber.tag(tag).d("Compositions: $msg ${ref.value}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,16 +22,8 @@ enum class FeatureFlags(
|
|||
override val description: String? = null,
|
||||
override val defaultValue: Boolean = true
|
||||
) : Feature {
|
||||
CollapseRoomStateEvents(
|
||||
key = "feature.collapseroomstateevents",
|
||||
title = "Collapse room state events",
|
||||
),
|
||||
ShowStartChatFlow(
|
||||
key = "feature.showstartchatflow",
|
||||
title = "Show start chat flow",
|
||||
),
|
||||
ShowMediaUploadingFlow(
|
||||
key = "feature.showmediauploadingflow",
|
||||
title = "Show media uploading flow",
|
||||
LocationSharing(
|
||||
key = "feature.locationsharing",
|
||||
title = "Allow user to share location",
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() :
|
|||
override suspend fun isFeatureEnabled(feature: Feature): Boolean {
|
||||
return if (feature is FeatureFlags) {
|
||||
when (feature) {
|
||||
FeatureFlags.CollapseRoomStateEvents -> false
|
||||
FeatureFlags.ShowStartChatFlow -> false
|
||||
FeatureFlags.ShowMediaUploadingFlow -> false
|
||||
FeatureFlags.LocationSharing -> true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ class DefaultFeatureFlagServiceTest {
|
|||
@Test
|
||||
fun `given service without provider when feature is checked then it returns the default value`() = runTest {
|
||||
val featureFlagService = DefaultFeatureFlagService(emptySet())
|
||||
val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)
|
||||
assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.ShowStartChatFlow.defaultValue)
|
||||
val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
|
||||
assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.LocationSharing.defaultValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given service without provider when set enabled feature is called then it returns false`() = runTest {
|
||||
val featureFlagService = DefaultFeatureFlagService(emptySet())
|
||||
val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
|
||||
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
|
||||
assertThat(result).isEqualTo(false)
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class DefaultFeatureFlagServiceTest {
|
|||
fun `given service with a runtime provider when set enabled feature is called then it returns true`() = runTest {
|
||||
val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0)
|
||||
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
|
||||
val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
|
||||
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
|
||||
assertThat(result).isEqualTo(true)
|
||||
}
|
||||
|
||||
|
|
@ -49,10 +49,10 @@ class DefaultFeatureFlagServiceTest {
|
|||
fun `given service with a runtime provider and feature enabled when feature is checked then it returns the correct value`() = runTest {
|
||||
val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0)
|
||||
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider))
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true)
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(false)
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true)
|
||||
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -60,8 +60,8 @@ class DefaultFeatureFlagServiceTest {
|
|||
val lowPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(LOW_PRIORITY)
|
||||
val highPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(HIGH_PRIORITY)
|
||||
val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityfeatureFlagProvider, highPriorityfeatureFlagProvider))
|
||||
lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false)
|
||||
highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true)
|
||||
lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false)
|
||||
highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true)
|
||||
assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package io.element.android.libraries.maplibre.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal val DefaultMapLocationSettings = MapLocationSettings()
|
||||
|
||||
/**
|
||||
|
|
@ -28,4 +30,11 @@ internal val DefaultMapLocationSettings = MapLocationSettings()
|
|||
*/
|
||||
public data class MapLocationSettings(
|
||||
public val locationEnabled: Boolean = false,
|
||||
public val backgroundTintColor: Color = Color.Unspecified,
|
||||
public val foregroundTintColor: Color = Color.Unspecified,
|
||||
public val backgroundStaleTintColor: Color = Color.Unspecified,
|
||||
public val foregroundStaleTintColor: Color = Color.Unspecified,
|
||||
public val accuracyColor: Color = Color.Unspecified,
|
||||
public val pulseEnabled: Boolean = false,
|
||||
public val pulseColor: Color = Color.Unspecified
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ internal class MapPropertiesNode(
|
|||
style: Style,
|
||||
context: Context,
|
||||
cameraPositionState: CameraPositionState,
|
||||
locationSettings: MapLocationSettings,
|
||||
) : MapNode {
|
||||
|
||||
init {
|
||||
|
|
@ -46,7 +47,13 @@ internal class MapPropertiesNode(
|
|||
LocationComponentActivationOptions.Builder(context, style)
|
||||
.locationComponentOptions(
|
||||
LocationComponentOptions.builder(context)
|
||||
.pulseEnabled(true)
|
||||
.backgroundTintColor(locationSettings.backgroundTintColor.toArgb())
|
||||
.foregroundTintColor(locationSettings.foregroundTintColor.toArgb())
|
||||
.backgroundStaleTintColor(locationSettings.backgroundStaleTintColor.toArgb())
|
||||
.foregroundStaleTintColor(locationSettings.foregroundStaleTintColor.toArgb())
|
||||
.accuracyColor(locationSettings.accuracyColor.toArgb())
|
||||
.pulseEnabled(locationSettings.pulseEnabled)
|
||||
.pulseColor(locationSettings.pulseColor.toArgb())
|
||||
.build()
|
||||
)
|
||||
.locationEngineRequest(
|
||||
|
|
@ -116,9 +123,9 @@ internal class MapPropertiesNode(
|
|||
@Composable
|
||||
internal inline fun MapUpdater(
|
||||
cameraPositionState: CameraPositionState,
|
||||
mapLocationSettings: MapLocationSettings,
|
||||
mapUiSettings: MapUiSettings,
|
||||
mapSymbolManagerSettings: MapSymbolManagerSettings,
|
||||
locationSettings: MapLocationSettings,
|
||||
uiSettings: MapUiSettings,
|
||||
symbolManagerSettings: MapSymbolManagerSettings,
|
||||
) {
|
||||
val mapApplier = currentComposer.applier as MapApplier
|
||||
val map = mapApplier.map
|
||||
|
|
@ -132,21 +139,22 @@ internal inline fun MapUpdater(
|
|||
style = style,
|
||||
context = context,
|
||||
cameraPositionState = cameraPositionState,
|
||||
locationSettings = locationSettings,
|
||||
)
|
||||
},
|
||||
update = {
|
||||
set(mapLocationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
|
||||
set(locationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
|
||||
|
||||
set(mapUiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
|
||||
set(mapUiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
|
||||
set(mapUiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
|
||||
set(mapUiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
|
||||
set(mapUiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
|
||||
set(mapUiSettings.logoGravity) { map.uiSettings.logoGravity = it }
|
||||
set(mapUiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
|
||||
set(mapUiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
|
||||
set(uiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
|
||||
set(uiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
|
||||
set(uiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
|
||||
set(uiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
|
||||
set(uiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
|
||||
set(uiSettings.logoGravity) { map.uiSettings.logoGravity = it }
|
||||
set(uiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
|
||||
set(uiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
|
||||
|
||||
set(mapSymbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
|
||||
set(symbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
|
||||
|
||||
update(cameraPositionState) { this.cameraPositionState = it }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,9 +124,9 @@ public fun MapboxMap(
|
|||
) {
|
||||
MapUpdater(
|
||||
cameraPositionState = currentCameraPositionState,
|
||||
mapUiSettings = currentUiSettings,
|
||||
mapLocationSettings = currentMapLocationSettings,
|
||||
mapSymbolManagerSettings = currentSymbolManagerSettings,
|
||||
uiSettings = currentUiSettings,
|
||||
locationSettings = currentMapLocationSettings,
|
||||
symbolManagerSettings = currentSymbolManagerSettings,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalCameraPositionState provides cameraPositionState,
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ sealed class AuthenticationException(message: String) : Exception(message) {
|
|||
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
|
||||
class SessionMissing(message: String) : AuthenticationException(message)
|
||||
class Generic(message: String) : AuthenticationException(message)
|
||||
class OidcError(type: String, message: String) : AuthenticationException(message)
|
||||
// TODO Oidc
|
||||
// class OidcError(type: String, message: String) : AuthenticationException(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ object PermalinkBuilder {
|
|||
|
||||
private const val ROOM_PATH = "room/"
|
||||
private const val USER_PATH = "user/"
|
||||
private const val GROUP_PATH = "group/"
|
||||
|
||||
private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.matrixToPermalinkBaseUrl).also {
|
||||
var baseUrl = it
|
||||
|
|
|
|||
|
|
@ -63,7 +63,11 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
val timeline: MatrixTimeline
|
||||
|
||||
fun open(): Result<Unit>
|
||||
fun destroy()
|
||||
|
||||
fun subscribeToSync()
|
||||
|
||||
fun unsubscribeFromSync()
|
||||
|
||||
suspend fun userDisplayName(userId: UserId): Result<String?>
|
||||
|
||||
|
|
@ -133,6 +137,8 @@ interface MatrixRoom : Closeable {
|
|||
zoomLevel: Int? = null,
|
||||
assetType: AssetType? = null,
|
||||
): Result<Unit>
|
||||
|
||||
override fun close() = destroy()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.auth
|
||||
|
||||
import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
// TODO Oidc
|
||||
// import io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||
// import org.matrix.rustcomponents.sdk.OidcClientMetadata
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.libraries.matrix.impl.media
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
|
||||
|
||||
fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
||||
|
|
|
|||
|
|
@ -40,9 +40,6 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher
|
|||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
||||
import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow
|
||||
import io.element.android.libraries.matrix.impl.timeline.eventOrigin
|
||||
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
|
||||
import io.element.android.libraries.sessionstorage.api.SessionData
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -51,11 +48,7 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.EventItemOrigin
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
|
|
@ -88,7 +81,6 @@ class RustMatrixRoom(
|
|||
|
||||
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
||||
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
|
||||
private val isInit = MutableStateFlow(false)
|
||||
private val _syncUpdateFlow = MutableStateFlow(0L)
|
||||
private val _timeline by lazy {
|
||||
RustMatrixTimeline(
|
||||
|
|
@ -97,6 +89,7 @@ class RustMatrixRoom(
|
|||
roomCoroutineScope = roomCoroutineScope,
|
||||
dispatcher = roomDispatcher,
|
||||
lastLoginTimestamp = sessionData.loginTimestamp,
|
||||
onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +99,7 @@ class RustMatrixRoom(
|
|||
|
||||
override val timeline: MatrixTimeline = _timeline
|
||||
|
||||
override fun open(): Result<Unit> {
|
||||
if (isInit.value) return Result.failure(IllegalStateException("Listener already registered"))
|
||||
override fun subscribeToSync() {
|
||||
val settings = RoomSubscription(
|
||||
requiredState = listOf(
|
||||
RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
|
||||
|
|
@ -118,37 +110,16 @@ class RustMatrixRoom(
|
|||
timelineLimit = null
|
||||
)
|
||||
roomListItem.subscribe(settings)
|
||||
roomCoroutineScope.launch(roomDispatcher) {
|
||||
innerRoom.timelineDiffFlow { initialList ->
|
||||
_timeline.postItems(initialList)
|
||||
}.onEach { diff ->
|
||||
diff.use {
|
||||
if (diff.eventOrigin() == EventItemOrigin.SYNC) {
|
||||
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||
}
|
||||
_timeline.postDiff(diff)
|
||||
}
|
||||
}.launchIn(this)
|
||||
|
||||
innerRoom.backPaginationStatusFlow()
|
||||
.onEach {
|
||||
_timeline.postPaginationStatus(it)
|
||||
}.launchIn(this)
|
||||
|
||||
fetchMembers()
|
||||
}
|
||||
isInit.value = true
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
if (isInit.value) {
|
||||
isInit.value = false
|
||||
roomCoroutineScope.cancel()
|
||||
roomListItem.unsubscribe()
|
||||
innerRoom.destroy()
|
||||
roomListItem.destroy()
|
||||
}
|
||||
override fun unsubscribeFromSync() {
|
||||
roomListItem.unsubscribe()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
roomCoroutineScope.cancel()
|
||||
innerRoom.destroy()
|
||||
roomListItem.destroy()
|
||||
}
|
||||
|
||||
override val name: String?
|
||||
|
|
@ -365,12 +336,6 @@ class RustMatrixRoom(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchMembers() = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatching {
|
||||
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.BackPaginationStatus
|
||||
import org.matrix.rustcomponents.sdk.EventItemOrigin
|
||||
import org.matrix.rustcomponents.sdk.PaginationOptions
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
|
|
@ -59,6 +63,7 @@ class RustMatrixTimeline(
|
|||
private val innerRoom: Room,
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val lastLoginTimestamp: Date?,
|
||||
private val onNewSyncedEvent: () -> Unit,
|
||||
) : MatrixTimeline {
|
||||
|
||||
private val initLatch = CompletableDeferred<Unit>()
|
||||
|
|
@ -95,13 +100,40 @@ class RustMatrixTimeline(
|
|||
|
||||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
|
||||
|
||||
init {
|
||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
||||
roomCoroutineScope.launch(dispatcher) {
|
||||
innerRoom.timelineDiffFlow { initialList ->
|
||||
postItems(initialList)
|
||||
}.onEach { diff ->
|
||||
if (diff.eventOrigin() == EventItemOrigin.SYNC) {
|
||||
onNewSyncedEvent()
|
||||
}
|
||||
postDiff(diff)
|
||||
}.launchIn(this)
|
||||
|
||||
innerRoom.backPaginationStatusFlow()
|
||||
.onEach {
|
||||
postPaginationStatus(it)
|
||||
}.launchIn(this)
|
||||
|
||||
fetchMembers()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchMembers() = withContext(dispatcher) {
|
||||
runCatching {
|
||||
innerRoom.fetchMembers()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.sample(50)
|
||||
.mapLatest { items ->
|
||||
encryptedHistoryPostProcessor.process(items)
|
||||
}
|
||||
|
||||
internal suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
||||
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
||||
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
||||
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
||||
ensureActive()
|
||||
|
|
@ -111,12 +143,12 @@ class RustMatrixTimeline(
|
|||
initLatch.complete(Unit)
|
||||
}
|
||||
|
||||
internal suspend fun postDiff(timelineDiff: TimelineDiff) {
|
||||
private suspend fun postDiff(timelineDiff: TimelineDiff) {
|
||||
initLatch.await()
|
||||
timelineDiffProcessor.postDiff(timelineDiff)
|
||||
}
|
||||
|
||||
internal fun postPaginationStatus(status: BackPaginationStatus) {
|
||||
private fun postPaginationStatus(status: BackPaginationStatus) {
|
||||
_paginationState.getAndUpdate { currentPaginationState ->
|
||||
if (hasEncryptionHistoryBanner()) {
|
||||
return@getAndUpdate currentPaginationState.copy(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class EventMessageMapper {
|
|||
fun map(message: Message): MessageContent = message.use {
|
||||
val type = it.msgtype().use(this::mapMessageType)
|
||||
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)
|
||||
val inReplyToEvent: InReplyTo? = (it.inReplyTo()?.event)?.use { details ->
|
||||
val inReplyToEvent: InReplyTo? = it.inReplyTo()?.event?.use { details ->
|
||||
when (details) {
|
||||
is RepliedToEventDetails.Ready -> {
|
||||
val senderProfile = details.senderProfile as? ProfileDetails.Ready
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
class TimelineEncryptedHistoryPostProcessor(
|
||||
private val lastLoginTimestamp: Date?,
|
||||
|
|
@ -70,5 +69,4 @@ class TimelineEncryptedHistoryPostProcessor(
|
|||
val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false
|
||||
return timestamp <= lastLoginTimestamp!!.time
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.core.SpaceId
|
|||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import java.util.UUID
|
||||
|
||||
const val A_USER_NAME = "alice"
|
||||
const val A_PASSWORD = "password"
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ class FakeMatrixRoom(
|
|||
private val _sentLocations = mutableListOf<SendLocationInvocation>()
|
||||
val sentLocations: List<SendLocationInvocation> = _sentLocations
|
||||
|
||||
|
||||
var invitedUserId: UserId? = null
|
||||
private set
|
||||
|
||||
|
|
@ -128,9 +127,11 @@ class FakeMatrixRoom(
|
|||
|
||||
override val timeline: MatrixTimeline = matrixTimeline
|
||||
|
||||
override fun open(): Result<Unit> {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
override fun subscribeToSync() = Unit
|
||||
|
||||
override fun unsubscribeFromSync() = Unit
|
||||
|
||||
override fun destroy() = Unit
|
||||
|
||||
override suspend fun userDisplayName(userId: UserId): Result<String?> = simulateLongTask {
|
||||
userDisplayNameResult
|
||||
|
|
@ -283,8 +284,6 @@ class FakeMatrixRoom(
|
|||
return sendLocationResult
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
this.leaveRoomError = throwable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ fun SelectedUsersList(
|
|||
val maxVisibleUsers = rowWidth / userWidthWithSpacing
|
||||
|
||||
// Round down the number of visible users to end with a state where one is half visible
|
||||
val targetFraction = (userWidth / 2) / userWidthWithSpacing
|
||||
val targetFraction = userWidth / 2 / userWidthWithSpacing
|
||||
val targetUsers = floor(maxVisibleUsers - targetFraction) + targetFraction
|
||||
|
||||
// Work out how much extra spacing we need to reduce the number of users that much, then split it evenly amongst the visible users
|
||||
|
|
@ -153,7 +153,7 @@ private fun ContentToPreview() {
|
|||
SelectedUsersList(
|
||||
selectedUsers = aMatrixUserList().take(6).toImmutableList(),
|
||||
modifier = Modifier
|
||||
.width((200 + (i * 20)).dp)
|
||||
.width((200 + i * 20).dp)
|
||||
.border(1.dp, Color.Red)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ class MediaSender @Inject constructor(
|
|||
progressCallback = progressCallback
|
||||
)
|
||||
}
|
||||
else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $uploadInfo"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.libraries.mediaupload
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
|
||||
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
||||
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
|
||||
|
|
@ -92,7 +91,7 @@ class ImageCompressor @Inject constructor(
|
|||
) {
|
||||
val (width, height) = when (resizeMode) {
|
||||
is ResizeMode.Approximate -> resizeMode.desiredWidth to resizeMode.desiredHeight
|
||||
is ResizeMode.Strict -> (resizeMode.maxWidth / 2) to (resizeMode.maxHeight / 2)
|
||||
is ResizeMode.Strict -> resizeMode.maxWidth / 2 to resizeMode.maxHeight / 2
|
||||
is ResizeMode.None -> return
|
||||
}
|
||||
// Read bounds only
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ class ThumbnailFactory @Inject constructor(
|
|||
cancellationSignal
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
ThumbnailUtils.createImageThumbnail(
|
||||
file.path,
|
||||
MediaStore.Images.Thumbnails.MINI_KIND,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,5 @@ dependencies {
|
|||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
package io.element.android.libraries.permissions.impl
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -41,7 +41,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -64,7 +64,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -84,7 +84,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -113,7 +113,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -142,7 +142,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
|
@ -164,7 +164,7 @@ class DefaultPermissionsPresenterTest {
|
|||
permissionsStore,
|
||||
permissionStateProvider
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.permissions.noop
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
|
|
@ -27,7 +27,7 @@ class NoopPermissionsPresenterTest {
|
|||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = NoopPermissionsPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
|
|||
|
|
@ -52,9 +52,6 @@ dependencies {
|
|||
implementation(projects.services.appnavstate.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
// TODO Temporary use the deprecated LocalBroadcastManager, to be changed later.
|
||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.truth)
|
||||
|
|
|
|||
|
|
@ -103,33 +103,6 @@ class PushersManager @Inject constructor(
|
|||
return "{\"cs\":\"$secretForUser\"}"
|
||||
}
|
||||
|
||||
suspend fun registerEmailForPush(email: String) {
|
||||
TODO()
|
||||
/*
|
||||
val currentSession = activeSessionHolder.getActiveSession()
|
||||
val appName = appNameProvider.getAppName()
|
||||
currentSession.pushersService().addEmailPusher(
|
||||
email = email,
|
||||
lang = localeProvider.current().language,
|
||||
emailBranding = appName,
|
||||
appDisplayName = appName,
|
||||
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
fun getPusherForCurrentSession() {}/*: Pusher? {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return null
|
||||
val deviceId = session.sessionParams.deviceId
|
||||
return session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId }
|
||||
}
|
||||
*/
|
||||
|
||||
suspend fun unregisterEmailPusher(email: String) {
|
||||
// val currentSession = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
// currentSession.pushersService().removeEmailPusher(email)
|
||||
}
|
||||
|
||||
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) {
|
||||
matrixClient.pushersService().unsetHttpPusher()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,17 +23,17 @@ import io.element.android.libraries.core.data.tryOrNull
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.api.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.currentSessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -48,7 +48,6 @@ import javax.inject.Inject
|
|||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultNotificationDrawerManager @Inject constructor(
|
||||
private val pushDataStore: PushDataStore,
|
||||
private val notifiableEventProcessor: NotifiableEventProcessor,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
private val notificationEventPersistence: NotificationEventPersistence,
|
||||
|
|
@ -76,9 +75,16 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private var currentAppNavigationState: NavigationState? = null
|
||||
|
||||
private fun onAppNavigationStateChange(navigationState: NavigationState) {
|
||||
when (navigationState) {
|
||||
NavigationState.Root -> {}
|
||||
NavigationState.Root -> {
|
||||
currentAppNavigationState?.currentSessionId()?.let { sessionId ->
|
||||
// User signed out, clear all notifications related to the session.
|
||||
clearAllEvents(sessionId)
|
||||
}
|
||||
}
|
||||
is NavigationState.Session -> {}
|
||||
is NavigationState.Space -> {}
|
||||
is NavigationState.Room -> {
|
||||
|
|
@ -93,6 +99,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
currentAppNavigationState = navigationState
|
||||
}
|
||||
|
||||
private fun createInitialNotificationState(): NotificationState {
|
||||
|
|
@ -133,12 +140,21 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
/**
|
||||
* Clear all known events and refresh the notification drawer.
|
||||
*/
|
||||
fun clearAllEvents(sessionId: SessionId) {
|
||||
fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
updateEvents {
|
||||
it.clearMessagesForSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications related to the session and refresh the notification drawer.
|
||||
*/
|
||||
fun clearAllEvents(sessionId: SessionId) {
|
||||
updateEvents {
|
||||
it.clearAllForSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when the application is currently opened and showing timeline for the given roomId.
|
||||
* Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class FilteredEventDetector @Inject constructor(
|
|||
* Returns true if the given event should be ignored.
|
||||
* Used to skip notifications if a non expected message is received.
|
||||
*/
|
||||
fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean {
|
||||
fun shouldBeIgnored(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
|
||||
/* TODO EAx
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
|
|
@ -57,9 +56,6 @@ private val loggerTag = LoggerTag("NotifiableEventResolver", pushLoggerTag)
|
|||
*/
|
||||
class NotifiableEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
// private val noticeEventFormatter: NoticeEventFormatter,
|
||||
// private val displayableEventFormatter: DisplayableEventFormatter,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val clock: SystemClock,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -19,15 +19,12 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.RemoteInput
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.log.notificationLoggerTag
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -39,10 +36,6 @@ private val loggerTag = LoggerTag("NotificationBroadcastReceiver", notificationL
|
|||
class NotificationBroadcastReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var defaultNotificationDrawerManager: DefaultNotificationDrawerManager
|
||||
|
||||
//@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
//@Inject lateinit var analyticsTracker: AnalyticsTracker
|
||||
@Inject lateinit var clock: SystemClock
|
||||
@Inject lateinit var actionIds: NotificationActionIds
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
|
|
@ -59,7 +52,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
|
||||
}
|
||||
actionIds.dismissSummary ->
|
||||
defaultNotificationDrawerManager.clearAllEvents(sessionId)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(sessionId)
|
||||
actionIds.dismissInvite -> if (roomId != null) {
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
|
||||
}
|
||||
|
|
@ -81,6 +74,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
|
|
@ -94,10 +88,10 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getSafeActiveSession()?.let { session ->
|
||||
|
|
@ -109,6 +103,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
*/
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) {
|
||||
/*
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
|
|
@ -123,7 +118,9 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
*/
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handleSmartReply(intent: Intent, context: Context) {
|
||||
/*
|
||||
val message = getReplyMessage(intent)
|
||||
val sessionId = intent.getStringExtra(KEY_SESSION_ID)?.let(::SessionId)
|
||||
val roomId = intent.getStringExtra(KEY_ROOM_ID)?.let(::RoomId)
|
||||
|
|
@ -134,13 +131,11 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
// Can this happen? should we update notification?
|
||||
return
|
||||
}
|
||||
/*
|
||||
activeSessionHolder.getActiveSession().let { session ->
|
||||
session.getRoom(roomId)?.let { room ->
|
||||
sendMatrixEvent(message, threadId, session, room, context)
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
|
|
@ -234,6 +229,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
*/
|
||||
|
||||
/*
|
||||
private fun getReplyMessage(intent: Intent?): String? {
|
||||
if (intent != null) {
|
||||
val remoteInput = RemoteInput.getResultsFromIntent(intent)
|
||||
|
|
@ -243,6 +239,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
|
|||
}
|
||||
return null
|
||||
}
|
||||
*/
|
||||
|
||||
companion object {
|
||||
const val KEY_SESSION_ID = "sessionID"
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@ data class NotificationEventQueue constructor(
|
|||
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
fun clearAllForSession(sessionId: SessionId) {
|
||||
Timber.d("clearAllForSession $sessionId")
|
||||
queue.removeAll { it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
Timber.d("clearMessageEventOfRoom $sessionId, $roomId")
|
||||
queue.removeAll { it is NotifiableMessageEvent && it.sessionId == sessionId && it.roomId == roomId }
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class OutdatedEventDetector @Inject constructor(
|
|||
* Used to clean up notifications if a displayed message has been read on an
|
||||
* other device.
|
||||
*/
|
||||
fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
|
||||
fun isMessageOutdated(@Suppress("UNUSED_PARAMETER") notifiableEvent: NotifiableEvent): Boolean {
|
||||
/* TODO EAx
|
||||
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
|
||||
val lastMessageTimestamp = events.last().timestamp
|
||||
val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||
val messageCount = (events.size - smartReplyErrors.size)
|
||||
val messageCount = events.size - smartReplyErrors.size
|
||||
val meta = RoomNotification.Message.Meta(
|
||||
summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup),
|
||||
messageCount = messageCount,
|
||||
|
|
|
|||
|
|
@ -19,12 +19,10 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
|
||||
class TestNotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// Internal broadcast to any one interested
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
// TODO The test notification has been clicked, notify the ui
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class NotificationChannels @Inject constructor(
|
|||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
fun openSystemSettingsForSilentCategory(activity: Activity) {
|
||||
activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
|
|
|
|||
|
|
@ -16,22 +16,17 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.PushersManager
|
||||
import io.element.android.libraries.push.impl.log.pushLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.store.DefaultPushDataStore
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import io.element.android.libraries.pushproviders.api.PushHandler
|
||||
|
|
@ -53,8 +48,7 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val defaultPushDataStore: DefaultPushDataStore,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val actionIds: NotificationActionIds,
|
||||
@ApplicationContext private val context: Context,
|
||||
// private val actionIds: NotificationActionIds,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
) : PushHandler {
|
||||
|
|
@ -82,8 +76,8 @@ class DefaultPushHandler @Inject constructor(
|
|||
|
||||
// Diagnostic Push
|
||||
if (pushData.eventId == PushersManager.TEST_EVENT_ID) {
|
||||
val intent = Intent(actionIds.push)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
// val intent = Intent(actionIds.push)
|
||||
// TODO The test push has been received, notify the ui
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 io.element.android.libraries.pushproviders.firebase
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO
|
||||
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
|
||||
// private val unifiedPushHelper: UnifiedPushHelper,
|
||||
// private val fcmHelper: FcmHelper,
|
||||
// private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
// fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
|
||||
// if (unifiedPushHelper.isEmbeddedDistributor()) {
|
||||
// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
|
||||
/*
|
||||
TODO EAx
|
||||
val currentSession = activeSessionHolder.getActiveSession()
|
||||
val currentPushers = currentSession.pushersService().getPushers()
|
||||
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
|
||||
*/
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
|
|||
@ApplicationContext private val context: Context,
|
||||
//private val pushDataStore: PushDataStore,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
|
||||
// private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver,
|
||||
) {
|
||||
|
||||
suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.push.pushstore.impl"
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
}
|
||||
}
|
||||
|
||||
anvil {
|
||||
|
|
@ -43,4 +48,13 @@ dependencies {
|
|||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
|
||||
androidTestImplementation(libs.coroutines.test)
|
||||
androidTestImplementation(libs.test.core)
|
||||
androidTestImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.test.truth)
|
||||
androidTestImplementation(libs.test.runner)
|
||||
androidTestImplementation(projects.libraries.sessionStorage.test)
|
||||
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 io.element.android.libraries.pushstore.impl
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* Note: to clear the emulator, invoke:
|
||||
* adb uninstall io.element.android.libraries.push.pushstore.impl.test
|
||||
*/
|
||||
class DefaultUserPushStoreFactoryTest {
|
||||
|
||||
/**
|
||||
* Ensure that creating UserPushStore is thread safe.
|
||||
*/
|
||||
@Test
|
||||
fun testParallelCreation() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
val sessionId = SessionId("@alice:server.org")
|
||||
val userPushStoreFactory = DefaultUserPushStoreFactory(context, NoOpSessionObserver())
|
||||
var userPushStore1: UserPushStore? = null
|
||||
val thread1 = thread {
|
||||
userPushStore1 = userPushStoreFactory.create(sessionId)
|
||||
}
|
||||
var userPushStore2: UserPushStore? = null
|
||||
val thread2 = thread {
|
||||
userPushStore2 = userPushStoreFactory.create(sessionId)
|
||||
}
|
||||
thread1.join()
|
||||
thread2.join()
|
||||
runBlocking {
|
||||
userPushStore1!!.areNotificationEnabledForDevice()
|
||||
userPushStore2!!.areNotificationEnabledForDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.pushstore.api.UserPushStore
|
|||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
|
|
@ -39,7 +40,7 @@ class DefaultUserPushStoreFactory @Inject constructor(
|
|||
}
|
||||
|
||||
// We can have only one class accessing a single data store, so keep a cache of them.
|
||||
private val cache = mutableMapOf<SessionId, UserPushStore>()
|
||||
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
|
||||
override fun create(userId: SessionId): UserPushStore {
|
||||
return cache.getOrPut(userId) {
|
||||
UserPushStoreDataStore(
|
||||
|
|
|
|||
26
libraries/session-storage/test/build.gradle.kts
Normal file
26
libraries/session-storage/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.sessionstorage.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 io.element.android.libraries.sessionstorage.test.observer
|
||||
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
|
||||
class NoOpSessionObserver : SessionObserver {
|
||||
override fun addListener(listener: SessionListener) = Unit
|
||||
override fun removeListener(listener: SessionListener) = Unit
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="rich_text_editor_a11y_add_attachment">"Pridať prílohu"</string>
|
||||
<string name="rich_text_editor_bullet_list">"Prepnúť zoznam odrážok"</string>
|
||||
<string name="rich_text_editor_code_block">"Prepnúť blok kódu"</string>
|
||||
<string name="rich_text_editor_composer_placeholder">"Správa…"</string>
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@
|
|||
<string name="error_failed_loading_map">"%1$s nedokázal načítať mapu. Skúste to prosím neskôr."</string>
|
||||
<string name="error_failed_loading_messages">"Načítanie správ zlyhalo"</string>
|
||||
<string name="error_failed_locating_user">"%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr."</string>
|
||||
<string name="error_missing_location_auth_android">"Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe z obrazovky nastavení."</string>
|
||||
<string name="error_missing_location_rationale_android">"Ak chcete odoslať polohu, povoľte %1$s prístup k vašej polohe v nasledujúcom dialógovom okne."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Niektoré správy neboli odoslané"</string>
|
||||
<string name="error_unknown">"Prepáčte, vyskytla sa chyba"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Pripojte sa ku mne na %1$s"</string>
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@
|
|||
<string name="error_failed_loading_map">"%1$s could not load the map. Please try again later."</string>
|
||||
<string name="error_failed_loading_messages">"Failed loading messages"</string>
|
||||
<string name="error_failed_locating_user">"%1$s could not access your location. Please try again later."</string>
|
||||
<string name="error_missing_location_auth_android">"To send a location, allow %1$s to access your location from its settings screen."</string>
|
||||
<string name="error_missing_location_rationale_android">"To send a location, allow %1$s to access your location in the next dialog."</string>
|
||||
<string name="error_missing_location_auth_android">"%1$s does not have permission to access your location. You can enable access in Settings."</string>
|
||||
<string name="error_missing_location_rationale_android">"%1$s does not have permission to access your location. Enable access below."</string>
|
||||
<string name="error_some_messages_have_not_been_sent">"Some messages have not been sent"</string>
|
||||
<string name="error_unknown">"Sorry, an error occurred"</string>
|
||||
<string name="invite_friends_rich_title">"🔐️ Join me on %1$s"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue