[Room List] Show offline indicator when the device is offline (#239)
* Implement the network status indicator. * Add `networkmonitor` feature.
This commit is contained in:
parent
1a2007bfc2
commit
798bedcaed
33 changed files with 511 additions and 21 deletions
1
changelog.d/141.feature
Normal file
1
changelog.d/141.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add a `NetworkMonitor` component to track the network connection status
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation(projects.libraries.textcomposer)
|
implementation(projects.libraries.textcomposer)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
implementation(libs.accompanist.flowlayout)
|
implementation(libs.accompanist.flowlayout)
|
||||||
|
|
@ -53,6 +54,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
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.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -48,6 +51,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
private val composerPresenter: MessageComposerPresenter,
|
private val composerPresenter: MessageComposerPresenter,
|
||||||
private val timelinePresenter: TimelinePresenter,
|
private val timelinePresenter: TimelinePresenter,
|
||||||
private val actionListPresenter: ActionListPresenter,
|
private val actionListPresenter: ActionListPresenter,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -64,6 +68,10 @@ class MessagesPresenter @Inject constructor(
|
||||||
val roomAvatar: MutableState<AvatarData?> = remember {
|
val roomAvatar: MutableState<AvatarData?> = remember {
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
|
||||||
|
println(networkConnectionStatus)
|
||||||
|
|
||||||
LaunchedEffect(syncUpdateFlow) {
|
LaunchedEffect(syncUpdateFlow) {
|
||||||
roomAvatar.value =
|
roomAvatar.value =
|
||||||
AvatarData(
|
AvatarData(
|
||||||
|
|
@ -89,6 +97,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,6 @@ data class MessagesState(
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
|
val hasNetworkConnection: Boolean,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
override val values: Sequence<MessagesState>
|
override val values: Sequence<MessagesState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aMessagesState(),
|
aMessagesState(),
|
||||||
|
aMessagesState().copy(hasNetworkConnection = false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,5 +48,6 @@ fun aMessagesState() = MessagesState(
|
||||||
timelineItems = aTimelineItemList(aTimelineItemContent()),
|
timelineItems = aTimelineItemList(aTimelineItemContent()),
|
||||||
),
|
),
|
||||||
actionListState = anActionListState(),
|
actionListState = anActionListState(),
|
||||||
|
hasNetworkConnection = true,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
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 kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -112,12 +112,15 @@ fun MessagesView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
contentWindowInsets = WindowInsets.statusBars,
|
||||||
topBar = {
|
topBar = {
|
||||||
MessagesViewTopBar(
|
Column {
|
||||||
roomTitle = state.roomName,
|
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||||
roomAvatar = state.roomAvatar,
|
MessagesViewTopBar(
|
||||||
onBackPressed = onBackPressed,
|
roomTitle = state.roomName,
|
||||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
roomAvatar = state.roomAvatar,
|
||||||
)
|
onBackPressed = onBackPressed,
|
||||||
|
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
MessagesViewContent(
|
MessagesViewContent(
|
||||||
|
|
@ -208,7 +211,8 @@ fun MessagesViewTopBar(
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
windowInsets = WindowInsets(0.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
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.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -141,6 +142,7 @@ class MessagesPresenterTest {
|
||||||
composerPresenter = messageComposerPresenter,
|
composerPresenter = messageComposerPresenter,
|
||||||
timelinePresenter = timelinePresenter,
|
timelinePresenter = timelinePresenter,
|
||||||
actionListPresenter = actionListPresenter,
|
actionListPresenter = actionListPresenter,
|
||||||
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
features/networkmonitor/api/build.gradle.kts
Normal file
31
features/networkmonitor/api/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
features/networkmonitor/impl/build.gradle.kts
Normal file
38
features/networkmonitor/impl/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
19
features/networkmonitor/impl/src/main/AndroidManifest.xml
Normal file
19
features/networkmonitor/impl/src/main/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
features/networkmonitor/test/build.gradle.kts
Normal file
30
features/networkmonitor/test/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(libs.accompanist.placeholder)
|
implementation(libs.accompanist.placeholder)
|
||||||
api(projects.features.roomlist.api)
|
api(projects.features.roomlist.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
@ -60,6 +61,7 @@ dependencies {
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.dateformatter.test)
|
testImplementation(projects.libraries.dateformatter.test)
|
||||||
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.libraries.permissions.noop)
|
testImplementation(projects.libraries.permissions.noop)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
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.RoomListRoomSummary
|
||||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.MatrixUser
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -57,6 +59,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<RoomListState> {
|
) : Presenter<RoomListState> {
|
||||||
|
|
||||||
|
|
@ -71,6 +74,8 @@ class RoomListPresenter @Inject constructor(
|
||||||
.roomSummaries()
|
.roomSummaries()
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
|
|
||||||
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
|
||||||
|
|
||||||
Timber.v("RoomSummaries size = ${roomSummaries.size}")
|
Timber.v("RoomSummaries size = ${roomSummaries.size}")
|
||||||
|
|
||||||
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
|
val filteredRoomSummaries: MutableState<ImmutableList<RoomListRoomSummary>> = remember {
|
||||||
|
|
@ -108,6 +113,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
displayVerificationPrompt = displayVerificationPrompt,
|
displayVerificationPrompt = displayVerificationPrompt,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ data class RoomListState(
|
||||||
val roomList: ImmutableList<RoomListRoomSummary>,
|
val roomList: ImmutableList<RoomListRoomSummary>,
|
||||||
val filter: String,
|
val filter: String,
|
||||||
val displayVerificationPrompt: Boolean,
|
val displayVerificationPrompt: Boolean,
|
||||||
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (RoomListEvents) -> Unit
|
val eventSink: (RoomListEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState(),
|
aRoomListState(),
|
||||||
aRoomListState().copy(displayVerificationPrompt = true),
|
aRoomListState().copy(displayVerificationPrompt = true),
|
||||||
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
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")),
|
matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("@id", "U")),
|
||||||
roomList = aRoomListRoomSummaryList(),
|
roomList = aRoomListRoomSummaryList(),
|
||||||
filter = "filter",
|
filter = "filter",
|
||||||
|
hasNetworkConnection = true,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
displayVerificationPrompt = false,
|
displayVerificationPrompt = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -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.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import io.element.android.libraries.designsystem.R as DrawableR
|
import io.element.android.libraries.designsystem.R as DrawableR
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
@ -150,14 +151,16 @@ fun RoomListContent(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
RoomListTopBar(
|
Column {
|
||||||
matrixUser = state.matrixUser,
|
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||||
filter = state.filter,
|
RoomListTopBar(
|
||||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
matrixUser = state.matrixUser,
|
||||||
onOpenSettings = onOpenSettings,
|
filter = state.filter,
|
||||||
scrollBehavior = scrollBehavior,
|
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||||
modifier = Modifier,
|
onOpenSettings = onOpenSettings,
|
||||||
)
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
package io.element.android.features.roomlist.impl.components
|
package io.element.android.features.roomlist.impl.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.ContentAlpha
|
import androidx.compose.material.ContentAlpha
|
||||||
import androidx.compose.material.icons.Icons
|
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.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.roomlist.impl.R
|
import io.element.android.features.roomlist.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -172,6 +174,7 @@ fun SearchRoomListTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
windowInsets = WindowInsets(0.dp)
|
||||||
)
|
)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
|
|
@ -231,6 +234,7 @@ private fun DefaultRoomListTopBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
windowInsets = WindowInsets(0.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
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.features.roomlist.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||||
|
|
@ -49,6 +50,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -76,6 +78,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -97,6 +100,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -122,6 +126,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -152,6 +157,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -187,6 +193,7 @@ class RoomListPresenterTests {
|
||||||
createDateFormatter(),
|
createDateFormatter(),
|
||||||
FakeRoomLastMessageFormatter(),
|
FakeRoomLastMessageFormatter(),
|
||||||
FakeSessionVerificationService(),
|
FakeSessionVerificationService(),
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -236,6 +243,7 @@ class RoomListPresenterTests {
|
||||||
givenIsReady(true)
|
givenIsReady(true)
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
},
|
},
|
||||||
|
FakeNetworkMonitor(),
|
||||||
SnackbarDispatcher(),
|
SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
|
||||||
|
|
@ -70,5 +70,10 @@ val Vermilion = Color(0xFFFF5B55)
|
||||||
|
|
||||||
val LinkColor = Color(0xFF0086E6)
|
val LinkColor = Color(0xFF0086E6)
|
||||||
|
|
||||||
|
// Compound colors
|
||||||
|
|
||||||
val TextColorCriticalLight = Color(0xFFD51928)
|
val TextColorCriticalLight = Color(0xFFD51928)
|
||||||
val TextColorCriticalDark = Color(0xfffd3e3c)
|
val TextColorCriticalDark = Color(0xfffd3e3c)
|
||||||
|
|
||||||
|
val Gray_400_Light = Color(0xFFE1E6EC)
|
||||||
|
val Gray_400_Dark = Color(0xFF26282D)
|
||||||
|
|
|
||||||
|
|
@ -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.DarkGrey
|
||||||
import io.element.android.libraries.designsystem.Gray_300
|
import io.element.android.libraries.designsystem.Gray_300
|
||||||
import io.element.android.libraries.designsystem.Gray_400
|
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.Gray_450
|
||||||
import io.element.android.libraries.designsystem.SystemGrey5Dark
|
import io.element.android.libraries.designsystem.SystemGrey5Dark
|
||||||
import io.element.android.libraries.designsystem.SystemGrey6Dark
|
import io.element.android.libraries.designsystem.SystemGrey6Dark
|
||||||
|
|
@ -38,6 +39,7 @@ fun elementColorsDark() = ElementColors(
|
||||||
messageHighlightedBackground = Azure,
|
messageHighlightedBackground = Azure,
|
||||||
quaternary = Gray_400,
|
quaternary = Gray_400,
|
||||||
quinary = Gray_450,
|
quinary = Gray_450,
|
||||||
|
gray400 = Gray_400_Dark,
|
||||||
textActionCritical = TextColorCriticalDark,
|
textActionCritical = TextColorCriticalDark,
|
||||||
isLight = false,
|
isLight = false,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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_150
|
||||||
import io.element.android.libraries.designsystem.Gray_200
|
import io.element.android.libraries.designsystem.Gray_200
|
||||||
import io.element.android.libraries.designsystem.Gray_25
|
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.Gray_50
|
||||||
import io.element.android.libraries.designsystem.SystemGrey5Light
|
import io.element.android.libraries.designsystem.SystemGrey5Light
|
||||||
import io.element.android.libraries.designsystem.SystemGrey6Light
|
import io.element.android.libraries.designsystem.SystemGrey6Light
|
||||||
|
|
@ -38,6 +39,7 @@ fun elementColorsLight() = ElementColors(
|
||||||
messageHighlightedBackground = Azure,
|
messageHighlightedBackground = Azure,
|
||||||
quaternary = Gray_100,
|
quaternary = Gray_100,
|
||||||
quinary = Gray_50,
|
quinary = Gray_50,
|
||||||
|
gray400 = Gray_400_Light,
|
||||||
textActionCritical = TextColorCriticalLight,
|
textActionCritical = TextColorCriticalLight,
|
||||||
isLight = true,
|
isLight = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ class ElementColors(
|
||||||
messageHighlightedBackground: Color,
|
messageHighlightedBackground: Color,
|
||||||
quaternary: Color,
|
quaternary: Color,
|
||||||
quinary: Color,
|
quinary: Color,
|
||||||
|
gray400: Color,
|
||||||
textActionCritical: Color,
|
textActionCritical: Color,
|
||||||
isLight: Boolean
|
isLight: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
@ -45,6 +46,9 @@ class ElementColors(
|
||||||
var quinary by mutableStateOf(quinary)
|
var quinary by mutableStateOf(quinary)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var gray400 by mutableStateOf(gray400)
|
||||||
|
private set
|
||||||
|
|
||||||
var textActionCritical by mutableStateOf(textActionCritical)
|
var textActionCritical by mutableStateOf(textActionCritical)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -57,6 +61,7 @@ class ElementColors(
|
||||||
messageHighlightedBackground: Color = this.messageHighlightedBackground,
|
messageHighlightedBackground: Color = this.messageHighlightedBackground,
|
||||||
quaternary: Color = this.quaternary,
|
quaternary: Color = this.quaternary,
|
||||||
quinary: Color = this.quinary,
|
quinary: Color = this.quinary,
|
||||||
|
gray400: Color = this.gray400,
|
||||||
textActionCritical: Color = this.textActionCritical,
|
textActionCritical: Color = this.textActionCritical,
|
||||||
isLight: Boolean = this.isLight,
|
isLight: Boolean = this.isLight,
|
||||||
) = ElementColors(
|
) = ElementColors(
|
||||||
|
|
@ -65,6 +70,7 @@ class ElementColors(
|
||||||
messageHighlightedBackground = messageHighlightedBackground,
|
messageHighlightedBackground = messageHighlightedBackground,
|
||||||
quaternary = quaternary,
|
quaternary = quaternary,
|
||||||
quinary = quinary,
|
quinary = quinary,
|
||||||
|
gray400 = gray400,
|
||||||
textActionCritical = textActionCritical,
|
textActionCritical = textActionCritical,
|
||||||
isLight = isLight,
|
isLight = isLight,
|
||||||
)
|
)
|
||||||
|
|
@ -75,6 +81,7 @@ class ElementColors(
|
||||||
messageHighlightedBackground = other.messageHighlightedBackground
|
messageHighlightedBackground = other.messageHighlightedBackground
|
||||||
quaternary = other.quaternary
|
quaternary = other.quaternary
|
||||||
quinary = other.quinary
|
quinary = other.quinary
|
||||||
|
gray400 = other.gray400
|
||||||
textActionCritical = other.textActionCritical
|
textActionCritical = other.textActionCritical
|
||||||
isLight = other.isLight
|
isLight = other.isLight
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ dependencies {
|
||||||
implementation(projects.libraries.dateformatter.impl)
|
implementation(projects.libraries.dateformatter.impl)
|
||||||
implementation(projects.features.roomlist.impl)
|
implementation(projects.features.roomlist.impl)
|
||||||
implementation(projects.features.login.impl)
|
implementation(projects.features.login.impl)
|
||||||
|
implementation(projects.features.networkmonitor.impl)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Modifier
|
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.DefaultRoomLastMessageFormatter
|
||||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||||
import io.element.android.features.roomlist.impl.RoomListView
|
import io.element.android.features.roomlist.impl.RoomListView
|
||||||
|
|
@ -45,11 +46,12 @@ class RoomListScreen(
|
||||||
private val dateFormatters = DateFormatters(locale, clock, timeZone)
|
private val dateFormatters = DateFormatters(locale, clock, timeZone)
|
||||||
private val sessionVerificationService = matrixClient.sessionVerificationService()
|
private val sessionVerificationService = matrixClient.sessionVerificationService()
|
||||||
private val presenter = RoomListPresenter(
|
private val presenter = RoomListPresenter(
|
||||||
matrixClient,
|
client = matrixClient,
|
||||||
DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
|
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
|
||||||
DefaultRoomLastMessageFormatter(context, matrixClient),
|
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(context, matrixClient),
|
||||||
sessionVerificationService,
|
sessionVerificationService = sessionVerificationService,
|
||||||
SnackbarDispatcher(),
|
networkMonitor = NetworkMonitorImpl(context),
|
||||||
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8b1012920df3d9e6754e43464723139ae3fd0c04a518f761afcd54e25105acd6
|
||||||
|
size 41437
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:59c0a62bf960d150ee111e88a932e9f5c85a82336a787b10db9f3c157ab13a35
|
||||||
|
size 40520
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cb089bb34d22666fda661f7a9e95eae8f39a39f6ec3d64d45f6a18cb681cf228
|
||||||
|
size 39817
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:54d8ec83ac35f79bb5d6a3dce30ba38a7e328e914454c98c2fd7a3a7e4a3fb46
|
||||||
|
size 39315
|
||||||
Loading…
Add table
Add a link
Reference in a new issue