[Room List] Show offline indicator when the device is offline (#239)

* Implement the network status indicator.

* Add `networkmonitor` feature.
This commit is contained in:
Jorge Martin Espinosa 2023-04-17 17:01:16 +02:00 committed by GitHub
parent 200763e15a
commit d391275420
33 changed files with 511 additions and 21 deletions

1
changelog.d/141.feature Normal file
View file

@ -0,0 +1 @@
Add a `NetworkMonitor` component to track the network connection status

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.textcomposer)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.features.networkmonitor.api)
implementation(libs.coil.compose)
implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)
@ -53,6 +54,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.networkmonitor.test)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -38,6 +39,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@ -48,6 +51,7 @@ class MessagesPresenter @Inject constructor(
private val composerPresenter: MessageComposerPresenter,
private val timelinePresenter: TimelinePresenter,
private val actionListPresenter: ActionListPresenter,
private val networkMonitor: NetworkMonitor,
) : Presenter<MessagesState> {
@Composable
@ -64,6 +68,10 @@ class MessagesPresenter @Inject constructor(
val roomAvatar: MutableState<AvatarData?> = remember {
mutableStateOf(null)
}
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
println(networkConnectionStatus)
LaunchedEffect(syncUpdateFlow) {
roomAvatar.value =
AvatarData(
@ -89,6 +97,7 @@ class MessagesPresenter @Inject constructor(
composerState = composerState,
timelineState = timelineState,
actionListState = actionListState,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
eventSink = ::handleEvents
)
}

View file

@ -31,5 +31,6 @@ data class MessagesState(
val composerState: MessageComposerState,
val timelineState: TimelineState,
val actionListState: ActionListState,
val hasNetworkConnection: Boolean,
val eventSink: (MessagesEvents) -> Unit
)

View file

@ -31,6 +31,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
override val values: Sequence<MessagesState>
get() = sequenceOf(
aMessagesState(),
aMessagesState().copy(hasNetworkConnection = false),
)
}
@ -47,5 +48,6 @@ fun aMessagesState() = MessagesState(
timelineItems = aTimelineItemList(aTimelineItemContent()),
),
actionListState = anActionListState(),
hasNetworkConnection = true,
eventSink = {}
)

View file

@ -70,7 +70,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import kotlinx.coroutines.launch
import timber.log.Timber
@ -112,12 +112,15 @@ fun MessagesView(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
topBar = {
MessagesViewTopBar(
roomTitle = state.roomName,
roomAvatar = state.roomAvatar,
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
)
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
MessagesViewTopBar(
roomTitle = state.roomName,
roomAvatar = state.roomAvatar,
onBackPressed = onBackPressed,
onRoomDetailsClicked = onRoomDetailsClicked,
)
}
},
content = { padding ->
MessagesViewContent(
@ -208,7 +211,8 @@ fun MessagesViewTopBar(
overflow = TextOverflow.Ellipsis
)
}
}
},
windowInsets = WindowInsets(0.dp)
)
}

View file

@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -141,6 +142,7 @@ class MessagesPresenterTest {
composerPresenter = messageComposerPresenter,
timelinePresenter = timelinePresenter,
actionListPresenter = actionListPresenter,
networkMonitor = FakeNetworkMonitor(),
)
}
}

View file

@ -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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.networkmonitor.api"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
}

View file

@ -0,0 +1,24 @@
/*
* 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.features.networkmonitor.api
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val connectivity: Flow<NetworkStatus>
val currentConnectivityStatus: NetworkStatus
}

View file

@ -0,0 +1,22 @@
/*
* 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.features.networkmonitor.api
enum class NetworkStatus {
Online,
Offline
}

View file

@ -0,0 +1,125 @@
/*
* 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.features.networkmonitor.api.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.WifiOff
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.ui.strings.R as StringR
@Composable
fun ConnectivityIndicatorView(
isOnline: Boolean,
modifier: Modifier = Modifier
) {
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline }
// Display the network indicator with an animation
AnimatedVisibility(
visibleState = isIndicatorVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Indicator(modifier)
}
// Show missing status bar padding when the indicator is not visible
AnimatedVisibility(
visibleState = isStatusBarPaddingVisible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
StatusBarPaddingSpacer(modifier)
}
}
@Composable
private fun Indicator(modifier: Modifier = Modifier) {
Row(
modifier
.fillMaxWidth()
.background(LocalColors.current.gray400)
.statusBarsPadding()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Bottom,
) {
val tint = MaterialTheme.colorScheme.primary
Image(
imageVector = Icons.Outlined.WifiOff,
contentDescription = null,
colorFilter = ColorFilter.tint(tint),
modifier = Modifier.size(16.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(StringR.string.common_offline), style = ElementTextStyles.Regular.bodyMD, color = tint)
}
}
@Composable
private fun StatusBarPaddingSpacer(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.statusBarsPadding())
}
@Preview
@Composable
internal fun PreviewLightConnectivityIndicatorView() {
ElementPreviewLight {
ConnectivityIndicatorView(isOnline = false)
}
}
@Preview
@Composable
internal fun PreviewDarkConnectivityIndicatorView() {
ElementPreviewDark {
ConnectivityIndicatorView(isOnline = false)
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
}
anvil {
generateDaggerFactories.set(true)
}
android {
namespace = "io.element.android.features.networkmonitor.impl"
}
dependencies {
implementation(libs.coroutines.core)
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.features.networkmonitor.api)
}

View file

@ -0,0 +1,19 @@
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

View file

@ -0,0 +1,93 @@
/*
* 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.features.networkmonitor.impl
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(scope = AppScope::class)
@SingleIn(AppScope::class)
class NetworkMonitorImpl @Inject constructor(
@ApplicationContext context: Context
) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
_connectivity.value = connectivityManager.currentConnectionStatus()
Timber.v("Connectivity status (available): ${connectivityManager.currentConnectionStatus()}")
}
override fun onLost(network: Network) {
_connectivity.value = connectivityManager.currentConnectionStatus()
Timber.v("Connectivity status (lost): ${connectivityManager.currentConnectionStatus()}")
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
_connectivity.value = connectivityManager.currentConnectionStatus()
Timber.v("Connectivity status (changed): ${connectivityManager.currentConnectionStatus()}")
}
}
private val _connectivity = MutableStateFlow(NetworkStatus.Online)
override val connectivity: Flow<NetworkStatus> = _connectivity
override val currentConnectivityStatus: NetworkStatus get() = _connectivity.value
init {
listenToConnectionChanges()
}
private fun listenToConnectionChanges() {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
_connectivity.tryEmit(connectivityManager.currentConnectionStatus())
}
private fun ConnectivityManager.currentConnectionStatus(): NetworkStatus {
val hasInternet = activeNetwork?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
return if (hasInternet) {
NetworkStatus.Online
} else {
NetworkStatus.Offline
}
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.networkmonitor.test"
}
dependencies {
api(projects.features.networkmonitor.api)
api(libs.coroutines.core)
}

View file

@ -0,0 +1,30 @@
/*
* 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.features.networkmonitor.test
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor(initialStatus: NetworkStatus = NetworkStatus.Online) : NetworkMonitor {
override val currentConnectivityStatus: NetworkStatus
get() = _connectivityStatus.value
private val _connectivityStatus: MutableStateFlow<NetworkStatus> = MutableStateFlow(initialStatus)
override val connectivity: Flow<NetworkStatus> = _connectivityStatus
}

View file

@ -48,6 +48,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.dateformatter.api)
implementation(projects.features.networkmonitor.api)
implementation(libs.accompanist.placeholder)
api(projects.features.roomlist.api)
ksp(libs.showkase.processor)
@ -60,6 +61,7 @@ dependencies {
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.libraries.permissions.noop)
androidTestImplementation(libs.test.junitext)

View file

@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
import io.element.android.libraries.architecture.Presenter
@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.ui.model.MatrixUser
import io.element.android.features.networkmonitor.api.NetworkStatus
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -57,6 +59,7 @@ class RoomListPresenter @Inject constructor(
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val sessionVerificationService: SessionVerificationService,
private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<RoomListState> {
@ -71,6 +74,8 @@ class RoomListPresenter @Inject constructor(
.roomSummaries()
.collectAsState()
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
Timber.v("RoomSummaries size = ${roomSummaries.size}")
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
@ -108,6 +113,7 @@ class RoomListPresenter @Inject constructor(
filter = filter,
displayVerificationPrompt = displayVerificationPrompt,
snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
eventSink = ::handleEvents
)
}

View file

@ -28,6 +28,7 @@ data class RoomListState(
val roomList: ImmutableList<RoomListRoomSummary>,
val filter: String,
val displayVerificationPrompt: Boolean,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val eventSink: (RoomListEvents) -> Unit
)

View file

@ -33,6 +33,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(),
aRoomListState().copy(displayVerificationPrompt = true),
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
aRoomListState().copy(hasNetworkConnection = false),
)
}
@ -40,6 +41,7 @@ internal fun aRoomListState() = RoomListState(
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("@id", "U")),
roomList = aRoomListRoomSummaryList(),
filter = "filter",
hasNetworkConnection = true,
snackbarMessage = null,
displayVerificationPrompt = false,
eventSink = {}

View file

@ -68,6 +68,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import kotlinx.coroutines.launch
import io.element.android.libraries.designsystem.R as DrawableR
import io.element.android.libraries.ui.strings.R as StringR
@ -150,14 +151,16 @@ fun RoomListContent(
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
RoomListTopBar(
matrixUser = state.matrixUser,
filter = state.filter,
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
modifier = Modifier,
)
Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
RoomListTopBar(
matrixUser = state.matrixUser,
filter = state.filter,
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
)
}
},
content = { padding ->
Column(

View file

@ -19,6 +19,7 @@
package io.element.android.features.roomlist.impl.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
@ -46,6 +47,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -172,6 +174,7 @@ fun SearchRoomListTopBar(
)
}
},
windowInsets = WindowInsets(0.dp)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
@ -231,6 +234,7 @@ private fun DefaultRoomListTopBar(
}
},
scrollBehavior = scrollBehavior,
windowInsets = WindowInsets(0.dp),
)
}

View file

@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
@ -49,6 +50,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -76,6 +78,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -97,6 +100,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -122,6 +126,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -152,6 +157,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -187,6 +193,7 @@ class RoomListPresenterTests {
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
@ -236,6 +243,7 @@ class RoomListPresenterTests {
givenIsReady(true)
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
},
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {

View file

@ -70,5 +70,10 @@ val Vermilion = Color(0xFFFF5B55)
val LinkColor = Color(0xFF0086E6)
// Compound colors
val TextColorCriticalLight = Color(0xFFD51928)
val TextColorCriticalDark = Color(0xfffd3e3c)
val Gray_400_Light = Color(0xFFE1E6EC)
val Gray_400_Dark = Color(0xFF26282D)

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.designsystem.Black_950
import io.element.android.libraries.designsystem.DarkGrey
import io.element.android.libraries.designsystem.Gray_300
import io.element.android.libraries.designsystem.Gray_400
import io.element.android.libraries.designsystem.Gray_400_Dark
import io.element.android.libraries.designsystem.Gray_450
import io.element.android.libraries.designsystem.SystemGrey5Dark
import io.element.android.libraries.designsystem.SystemGrey6Dark
@ -38,6 +39,7 @@ fun elementColorsDark() = ElementColors(
messageHighlightedBackground = Azure,
quaternary = Gray_400,
quinary = Gray_450,
gray400 = Gray_400_Dark,
textActionCritical = TextColorCriticalDark,
isLight = false,
)

View file

@ -26,6 +26,7 @@ import io.element.android.libraries.designsystem.Gray_100
import io.element.android.libraries.designsystem.Gray_150
import io.element.android.libraries.designsystem.Gray_200
import io.element.android.libraries.designsystem.Gray_25
import io.element.android.libraries.designsystem.Gray_400_Light
import io.element.android.libraries.designsystem.Gray_50
import io.element.android.libraries.designsystem.SystemGrey5Light
import io.element.android.libraries.designsystem.SystemGrey6Light
@ -38,6 +39,7 @@ fun elementColorsLight() = ElementColors(
messageHighlightedBackground = Azure,
quaternary = Gray_100,
quinary = Gray_50,
gray400 = Gray_400_Light,
textActionCritical = TextColorCriticalLight,
isLight = true,
)

View file

@ -29,6 +29,7 @@ class ElementColors(
messageHighlightedBackground: Color,
quaternary: Color,
quinary: Color,
gray400: Color,
textActionCritical: Color,
isLight: Boolean
) {
@ -45,6 +46,9 @@ class ElementColors(
var quinary by mutableStateOf(quinary)
private set
var gray400 by mutableStateOf(gray400)
private set
var textActionCritical by mutableStateOf(textActionCritical)
private set
@ -57,6 +61,7 @@ class ElementColors(
messageHighlightedBackground: Color = this.messageHighlightedBackground,
quaternary: Color = this.quaternary,
quinary: Color = this.quinary,
gray400: Color = this.gray400,
textActionCritical: Color = this.textActionCritical,
isLight: Boolean = this.isLight,
) = ElementColors(
@ -65,6 +70,7 @@ class ElementColors(
messageHighlightedBackground = messageHighlightedBackground,
quaternary = quaternary,
quinary = quinary,
gray400 = gray400,
textActionCritical = textActionCritical,
isLight = isLight,
)
@ -75,6 +81,7 @@ class ElementColors(
messageHighlightedBackground = other.messageHighlightedBackground
quaternary = other.quaternary
quinary = other.quinary
gray400 = other.gray400
textActionCritical = other.textActionCritical
isLight = other.isLight
}

View file

@ -56,6 +56,7 @@ dependencies {
implementation(projects.libraries.dateformatter.impl)
implementation(projects.features.roomlist.impl)
implementation(projects.features.login.impl)
implementation(projects.features.networkmonitor.impl)
implementation(libs.coroutines.core)
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
}

View file

@ -20,6 +20,7 @@ import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
import io.element.android.features.roomlist.impl.DefaultRoomLastMessageFormatter
import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListView
@ -45,11 +46,12 @@ class RoomListScreen(
private val dateFormatters = DateFormatters(locale, clock, timeZone)
private val sessionVerificationService = matrixClient.sessionVerificationService()
private val presenter = RoomListPresenter(
matrixClient,
DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
DefaultRoomLastMessageFormatter(context, matrixClient),
sessionVerificationService,
SnackbarDispatcher(),
client = matrixClient,
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(context, matrixClient),
sessionVerificationService = sessionVerificationService,
networkMonitor = NetworkMonitorImpl(context),
snackbarDispatcher = SnackbarDispatcher(),
)
@Composable

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8b1012920df3d9e6754e43464723139ae3fd0c04a518f761afcd54e25105acd6
size 41437

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59c0a62bf960d150ee111e88a932e9f5c85a82336a787b10db9f3c157ab13a35
size 40520

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb089bb34d22666fda661f7a9e95eae8f39a39f6ec3d64d45f6a18cb681cf228
size 39817

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54d8ec83ac35f79bb5d6a3dce30ba38a7e328e914454c98c2fd7a3a7e4a3fb46
size 39315