Merge branch 'develop' into feature/bma/sendImageFromKeyboard
This commit is contained in:
commit
cbc86ea1e4
759 changed files with 2603 additions and 1574 deletions
|
|
@ -34,7 +34,7 @@ import io.element.android.features.call.data.WidgetMessage
|
|||
import io.element.android.features.call.utils.CallWidgetProvider
|
||||
import io.element.android.features.call.utils.WidgetMessageInterceptor
|
||||
import io.element.android.features.call.utils.WidgetMessageSerializer
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -75,7 +75,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun present(): CallScreenState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val urlState = remember { mutableStateOf<Async<String>>(Async.Uninitialized) }
|
||||
val urlState = remember { mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized) }
|
||||
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
|
||||
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
|
||||
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
|
||||
|
|
@ -154,7 +154,7 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.loadUrl(
|
||||
inputs: CallType,
|
||||
urlState: MutableState<Async<String>>,
|
||||
urlState: MutableState<AsyncData<String>>,
|
||||
callWidgetDriver: MutableState<MatrixWidgetDriver?>,
|
||||
) = launch {
|
||||
urlState.runCatchingUpdatingState {
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
package io.element.android.features.call.ui
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class CallScreenState(
|
||||
val urlState: Async<String>,
|
||||
val urlState: AsyncData<String>,
|
||||
val userAgent: String,
|
||||
val isInWidgetMode: Boolean,
|
||||
val eventSink: (CallScreenEvents) -> Unit,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.call.R
|
||||
import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -96,7 +96,7 @@ internal fun CallScreenView(
|
|||
|
||||
@Composable
|
||||
private fun CallWebView(
|
||||
url: Async<String>,
|
||||
url: AsyncData<String>,
|
||||
userAgent: String,
|
||||
onPermissionsRequested: (PermissionRequest) -> Unit,
|
||||
onWebViewCreated: (WebView) -> Unit,
|
||||
|
|
@ -116,7 +116,7 @@ private fun CallWebView(
|
|||
}
|
||||
},
|
||||
update = { webView ->
|
||||
if (url is Async.Success && webView.url != url.data) {
|
||||
if (url is AsyncData.Success && webView.url != url.data) {
|
||||
webView.loadUrl(url.data)
|
||||
}
|
||||
},
|
||||
|
|
@ -161,7 +161,7 @@ internal fun CallScreenViewPreview() {
|
|||
ElementPreview {
|
||||
CallScreenView(
|
||||
state = CallScreenState(
|
||||
urlState = Async.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
urlState = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
isInWidgetMode = false,
|
||||
userAgent = "",
|
||||
eventSink = {},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.call.CallType
|
||||
import io.element.android.features.call.utils.FakeCallWidgetProvider
|
||||
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -62,7 +62,7 @@ class CallScreenPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.urlState).isEqualTo(Async.Success("https://call.element.io"))
|
||||
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
|
||||
assertThat(initialState.isInWidgetMode).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ class CallScreenPresenterTest {
|
|||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(initialState.isInWidgetMode).isTrue()
|
||||
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
||||
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.createroom.api
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
|
|
@ -27,5 +27,5 @@ interface StartDMAction {
|
|||
* @param userId The user to start a DM with.
|
||||
* @param actionState The state to update with the result of the action.
|
||||
*/
|
||||
suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>)
|
||||
suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import androidx.compose.runtime.MutableState
|
|||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -36,17 +36,17 @@ class DefaultStartDMAction @Inject constructor(
|
|||
private val analyticsService: AnalyticsService,
|
||||
) : StartDMAction {
|
||||
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) {
|
||||
actionState.value = Async.Loading()
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
|
||||
actionState.value = AsyncAction.Loading
|
||||
when (val result = matrixClient.startDM(userId)) {
|
||||
is StartDMResult.Success -> {
|
||||
if (result.isNew) {
|
||||
analyticsService.capture(CreatedRoom(isDM = true))
|
||||
}
|
||||
actionState.value = Async.Success(result.roomId)
|
||||
actionState.value = AsyncAction.Success(result.roomId)
|
||||
}
|
||||
is StartDMResult.Failure -> {
|
||||
actionState.value = Async.Failure(result.throwable)
|
||||
actionState.value = AsyncAction.Failure(result.throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
|
|
@ -49,6 +50,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
fun SearchUserBar(
|
||||
query: String,
|
||||
state: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
showLoader: Boolean,
|
||||
selectedUsers: ImmutableList<MatrixUser>,
|
||||
active: Boolean,
|
||||
isMultiSelectionEnabled: Boolean,
|
||||
|
|
@ -99,6 +101,11 @@ fun SearchUserBar(
|
|||
)
|
||||
}
|
||||
},
|
||||
contentSuffix = {
|
||||
if (showLoader) {
|
||||
AsyncLoading()
|
||||
}
|
||||
},
|
||||
resultState = state,
|
||||
resultHandler = { users ->
|
||||
LazyColumn(state = columnState) {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ fun UserListView(
|
|||
state = state.searchResults,
|
||||
selectedUsers = state.selectedUsers,
|
||||
active = state.isSearchActive,
|
||||
showLoader = state.showSearchLoader,
|
||||
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
|
||||
showBackButton = showBackButton,
|
||||
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
|
@ -91,10 +91,10 @@ class ConfigureRoomPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val createRoomAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
fun createRoom(config: CreateRoomConfig) {
|
||||
createRoomAction.value = Async.Uninitialized
|
||||
createRoomAction.value = AsyncAction.Uninitialized
|
||||
localCoroutineScope.createRoom(config, createRoomAction)
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = Async.Uninitialized
|
||||
ConfigureRoomEvents.CancelCreateRoom -> createRoomAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ class ConfigureRoomPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.createRoom(
|
||||
config: CreateRoomConfig,
|
||||
createRoomAction: MutableState<Async<RoomId>>
|
||||
createRoomAction: MutableState<AsyncAction<RoomId>>
|
||||
) = launch {
|
||||
suspend {
|
||||
val avatarUrl = config.avatarUri?.let { uploadAvatar(it) }
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
import io.element.android.libraries.permissions.api.PermissionsState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ConfigureRoomState(
|
||||
val config: CreateRoomConfig,
|
||||
val avatarActions: ImmutableList<AvatarAction>,
|
||||
val createRoomAction: Async<RoomId>,
|
||||
val createRoomAction: AsyncAction<RoomId>,
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl.configureroom
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.permissions.api.aPermissionsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
|
|||
fun aConfigureRoomState() = ConfigureRoomState(
|
||||
config = CreateRoomConfig(),
|
||||
avatarActions = persistentListOf(),
|
||||
createRoomAction = Async.Uninitialized,
|
||||
createRoomAction = AsyncAction.Uninitialized,
|
||||
cameraPermissionState = aPermissionsState(showDialog = false),
|
||||
eventSink = { },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ import io.element.android.compound.theme.ElementTheme
|
|||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
|
||||
import io.element.android.libraries.designsystem.components.LabelledTextField
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -148,9 +149,13 @@ fun ConfigureRoomView(
|
|||
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
||||
)
|
||||
|
||||
AsyncView(
|
||||
AsyncActionView(
|
||||
async = state.createRoomAction,
|
||||
progressText = stringResource(CommonStrings.common_creating_room),
|
||||
progressDialog = {
|
||||
AsyncActionViewDefaults.ProgressDialog(
|
||||
progressText = stringResource(CommonStrings.common_creating_room),
|
||||
)
|
||||
},
|
||||
onSuccess = { onRoomCreated(it) },
|
||||
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
|
||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import io.element.android.features.createroom.impl.userlist.SelectionMode
|
|||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenter
|
||||
import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -55,14 +55,14 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
val userListState = presenter.present()
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val startDmActionState: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvents(event: CreateRoomRootEvents) {
|
||||
when (event) {
|
||||
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
|
||||
startDMAction.execute(event.matrixUser.userId, startDmActionState)
|
||||
}
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = Async.Uninitialized
|
||||
CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@
|
|||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import io.element.android.features.createroom.impl.userlist.UserListState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class CreateRoomRootState(
|
||||
val applicationName: String,
|
||||
val userListState: UserListState,
|
||||
val startDmAction: Async<RoomId>,
|
||||
val startDmAction: AsyncAction<RoomId>,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ package io.element.android.features.createroom.impl.root
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.createroom.impl.userlist.aUserListState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
|
|
@ -30,7 +30,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
get() = sequenceOf(
|
||||
aCreateRoomRootState(),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Loading(),
|
||||
startDmAction = AsyncAction.Loading,
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
|
|
@ -41,7 +41,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
}
|
||||
),
|
||||
aCreateRoomRootState().copy(
|
||||
startDmAction = Async.Failure(Throwable("error")),
|
||||
startDmAction = AsyncAction.Failure(Throwable("error")),
|
||||
userListState = aMatrixUser().let {
|
||||
aUserListState().copy(
|
||||
searchQuery = it.userId.value,
|
||||
|
|
@ -57,6 +57,6 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
|
|||
fun aCreateRoomRootState() = CreateRoomRootState(
|
||||
eventSink = {},
|
||||
applicationName = "Element X Preview",
|
||||
startDmAction = Async.Uninitialized,
|
||||
startDmAction = AsyncAction.Uninitialized,
|
||||
userListState = aUserListState(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ import io.element.android.compound.theme.ElementTheme
|
|||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.createroom.impl.R
|
||||
import io.element.android.features.createroom.impl.components.UserListView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -93,9 +94,13 @@ fun CreateRoomRootView(
|
|||
}
|
||||
}
|
||||
|
||||
AsyncView(
|
||||
AsyncActionView(
|
||||
async = state.startDmAction,
|
||||
progressText = stringResource(CommonStrings.common_starting_chat),
|
||||
progressDialog = {
|
||||
AsyncActionViewDefaults.ProgressDialog(
|
||||
progressText = stringResource(CommonStrings.common_starting_chat),
|
||||
)
|
||||
},
|
||||
onSuccess = { onOpenDM(it) },
|
||||
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
|
||||
onRetry = {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import io.element.android.libraries.usersearch.api.UserRepository
|
|||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class DefaultUserListPresenter @AssistedInject constructor(
|
||||
@Assisted val args: UserListPresenterArgs,
|
||||
|
|
@ -57,18 +59,21 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults: SearchBarResultState<ImmutableList<UserSearchResult>> by remember {
|
||||
mutableStateOf(SearchBarResultState.NotSearching())
|
||||
mutableStateOf(SearchBarResultState.Initial())
|
||||
}
|
||||
var showSearchLoader by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
searchResults = SearchBarResultState.NotSearching()
|
||||
|
||||
userRepository.search(searchQuery).collect {
|
||||
searchResults = SearchBarResultState.Initial()
|
||||
showSearchLoader = false
|
||||
userRepository.search(searchQuery).onEach { state ->
|
||||
showSearchLoader = state.isSearching
|
||||
searchResults = when {
|
||||
it.isEmpty() -> SearchBarResultState.NoResults()
|
||||
else -> SearchBarResultState.Results(it.toImmutableList())
|
||||
state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
|
||||
state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
|
||||
else -> SearchBarResultState.Results(state.results.toImmutableList())
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
return UserListState(
|
||||
|
|
@ -76,6 +81,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
|
|||
searchResults = searchResults,
|
||||
selectedUsers = selectedUsers.toImmutableList(),
|
||||
isSearchActive = isSearchActive,
|
||||
showSearchLoader = showSearchLoader,
|
||||
selectionMode = args.selectionMode,
|
||||
eventSink = { event ->
|
||||
when (event) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class UserListState(
|
||||
val searchQuery: String,
|
||||
val searchResults: SearchBarResultState<ImmutableList<UserSearchResult>>,
|
||||
val showSearchLoader: Boolean,
|
||||
val selectedUsers: ImmutableList<MatrixUser>,
|
||||
val isSearchActive: Boolean,
|
||||
val selectionMode: SelectionMode,
|
||||
|
|
|
|||
|
|
@ -51,17 +51,19 @@ open class UserListStateProvider : PreviewParameterProvider<UserListState> {
|
|||
aUserListState().copy(
|
||||
isSearchActive = true,
|
||||
searchQuery = "something-with-no-results",
|
||||
searchResults = SearchBarResultState.NoResults()
|
||||
searchResults = SearchBarResultState.NoResultsFound()
|
||||
),
|
||||
aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single),
|
||||
)
|
||||
}
|
||||
|
||||
fun aUserListState() = UserListState(
|
||||
isSearchActive = false,
|
||||
searchQuery = "",
|
||||
searchResults = SearchBarResultState.NotSearching(),
|
||||
searchResults = SearchBarResultState.Initial(),
|
||||
selectedUsers = persistentListOf(),
|
||||
selectionMode = SelectionMode.Single,
|
||||
showSearchLoader = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.createroom.impl
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -39,9 +39,9 @@ class DefaultStartDMActionTests {
|
|||
givenFindDmResult(A_ROOM_ID)
|
||||
}
|
||||
val action = createStartDMAction(matrixClient)
|
||||
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID))
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -52,9 +52,9 @@ class DefaultStartDMActionTests {
|
|||
}
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val action = createStartDMAction(matrixClient, analyticsService)
|
||||
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID))
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Success(A_ROOM_ID))
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true))
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +65,9 @@ class DefaultStartDMActionTests {
|
|||
givenCreateDmResult(Result.failure(A_THROWABLE))
|
||||
}
|
||||
val action = createStartDMAction(matrixClient)
|
||||
val state = mutableStateOf<Async<RoomId>>(Async.Uninitialized)
|
||||
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
|
||||
action.execute(A_USER_ID, state)
|
||||
assertThat(state.value).isEqualTo(Async.Failure<RoomId>(A_THROWABLE))
|
||||
assertThat(state.value).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
}
|
||||
|
||||
private fun createStartDMAction(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import im.vector.app.features.analytics.plan.CreatedRoom
|
|||
import io.element.android.features.createroom.impl.CreateRoomConfig
|
||||
import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
|
|
@ -234,9 +234,9 @@ class ConfigureRoomPresenterTests {
|
|||
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
||||
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
assertThat(stateAfterCreateRoom.createRoomAction.dataOrNull()).isEqualTo(createRoomResult.getOrNull())
|
||||
}
|
||||
}
|
||||
|
|
@ -272,16 +272,16 @@ class ConfigureRoomPresenterTests {
|
|||
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
|
||||
|
||||
fakeMatrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,22 +297,22 @@ class ConfigureRoomPresenterTests {
|
|||
|
||||
// Create
|
||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterCreateRoom = awaitItem()
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat((stateAfterCreateRoom.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||
|
||||
// Retry
|
||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config))
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val stateAfterRetry = awaitItem()
|
||||
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat((stateAfterRetry.createRoomAction as? Async.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||
assertThat(stateAfterRetry.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||
|
||||
// Cancel
|
||||
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ import io.element.android.features.createroom.impl.userlist.FakeUserListPresente
|
|||
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
|
||||
import io.element.android.features.createroom.impl.userlist.UserListDataStore
|
||||
import io.element.android.features.createroom.test.FakeStartDMAction
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -52,20 +51,20 @@ class CreateRoomRootPresenterTests {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
|
||||
assertThat(initialState.startDmAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName)
|
||||
assertThat(initialState.userListState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse()
|
||||
|
||||
val matrixUser = MatrixUser(UserId("@name:domain"))
|
||||
val startDMSuccessResult = Async.Success(A_ROOM_ID)
|
||||
val startDMFailureResult = Async.Failure<RoomId>(A_THROWABLE)
|
||||
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
|
||||
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
|
||||
|
||||
// Failure
|
||||
startDMAction.givenExecuteResult(startDMFailureResult)
|
||||
initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(startDMFailureResult)
|
||||
state.eventSink(CreateRoomRootEvents.CancelStartDM)
|
||||
|
|
@ -74,10 +73,10 @@ class CreateRoomRootPresenterTests {
|
|||
// Success
|
||||
startDMAction.givenExecuteResult(startDMSuccessResult)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(state.startDmAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
state.eventSink(CreateRoomRootEvents.StartDM(matrixUser))
|
||||
}
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().startDmAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.startDmAction).isEqualTo(startDMSuccessResult)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
|
|||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResult
|
||||
import io.element.android.libraries.usersearch.api.UserSearchResultState
|
||||
import io.element.android.libraries.usersearch.test.FakeUserRepository
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -55,7 +56,7 @@ class DefaultUserListPresenterTests {
|
|||
assertThat(initialState.isMultiSelectionEnabled).isFalse()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ class DefaultUserListPresenterTests {
|
|||
assertThat(initialState.isMultiSelectionEnabled).isTrue()
|
||||
assertThat(initialState.isSearchActive).isFalse()
|
||||
assertThat(initialState.selectedUsers).isEmpty()
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,25 +132,38 @@ class DefaultUserListPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||
skipItems(2)
|
||||
|
||||
// When the user repository emits a result, it's copied to the state
|
||||
userRepository.emitResult(listOf(UserSearchResult(aMatrixUser())))
|
||||
assertThat(awaitItem().searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
persistentListOf(UserSearchResult(aMatrixUser()))
|
||||
)
|
||||
val result = UserSearchResultState(
|
||||
results = listOf(UserSearchResult(aMatrixUser())),
|
||||
isSearching = false,
|
||||
)
|
||||
|
||||
userRepository.emitState(result)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
persistentListOf(UserSearchResult(aMatrixUser()))
|
||||
)
|
||||
)
|
||||
assertThat(state.showSearchLoader).isFalse()
|
||||
}
|
||||
// When the user repository emits another result, it replaces the previous value
|
||||
userRepository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
|
||||
assertThat(awaitItem().searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
aMatrixUserList().map { UserSearchResult(it) }
|
||||
)
|
||||
val newResult = UserSearchResultState(
|
||||
results = aMatrixUserList().map { UserSearchResult(it) },
|
||||
isSearching = false,
|
||||
)
|
||||
userRepository.emitState(newResult)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.searchResults).isEqualTo(
|
||||
SearchBarResultState.Results(
|
||||
aMatrixUserList().map { UserSearchResult(it) }
|
||||
)
|
||||
)
|
||||
assertThat(state.showSearchLoader).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,13 +184,13 @@ class DefaultUserListPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
|
||||
initialState.eventSink(UserListEvents.UpdateSearchQuery("alice"))
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.NotSearching::class.java)
|
||||
assertThat(initialState.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||
assertThat(userRepository.providedQuery).isEqualTo("alice")
|
||||
skipItems(2)
|
||||
|
||||
// When the results list is empty, the state is set to NoResults
|
||||
userRepository.emitResult(emptyList())
|
||||
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResults::class.java)
|
||||
userRepository.emitState(UserSearchResultState(results = emptyList(), isSearching = false))
|
||||
assertThat(awaitItem().searchResults).isInstanceOf(SearchBarResultState.NoResultsFound::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.createroom.test
|
|||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import io.element.android.features.createroom.api.StartDMAction
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -26,14 +26,14 @@ import kotlinx.coroutines.delay
|
|||
|
||||
class FakeStartDMAction : StartDMAction {
|
||||
|
||||
private var executeResult: Async<RoomId> = Async.Success(A_ROOM_ID)
|
||||
private var executeResult: AsyncAction<RoomId> = AsyncAction.Success(A_ROOM_ID)
|
||||
|
||||
fun givenExecuteResult(result: Async<RoomId>) {
|
||||
fun givenExecuteResult(result: AsyncAction<RoomId>) {
|
||||
executeResult = result
|
||||
}
|
||||
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) {
|
||||
actionState.value = Async.Loading()
|
||||
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
|
||||
actionState.value = AsyncAction.Loading
|
||||
delay(1)
|
||||
actionState.value = executeResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
|
|||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -77,14 +77,14 @@ class InviteListPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val acceptedAction: MutableState<AsyncData<RoomId>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
val declinedAction: MutableState<AsyncData<Unit>> = remember { mutableStateOf(AsyncData.Uninitialized) }
|
||||
val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) }
|
||||
|
||||
fun handleEvent(event: InviteListEvents) {
|
||||
when (event) {
|
||||
is InviteListEvents.AcceptInvite -> {
|
||||
acceptedAction.value = Async.Uninitialized
|
||||
acceptedAction.value = AsyncData.Uninitialized
|
||||
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class InviteListPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
is InviteListEvents.ConfirmDeclineInvite -> {
|
||||
declinedAction.value = Async.Uninitialized
|
||||
declinedAction.value = AsyncData.Uninitialized
|
||||
decliningInvite.value?.let {
|
||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||
}
|
||||
|
|
@ -105,11 +105,11 @@ class InviteListPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
is InviteListEvents.DismissAcceptError -> {
|
||||
acceptedAction.value = Async.Uninitialized
|
||||
acceptedAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
|
||||
is InviteListEvents.DismissDeclineError -> {
|
||||
declinedAction.value = Async.Uninitialized
|
||||
declinedAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ class InviteListPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
|
||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncData<RoomId>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.join().getOrThrow()
|
||||
|
|
@ -148,7 +148,7 @@ class InviteListPresenter @Inject constructor(
|
|||
}.runCatchingUpdatingState(acceptedAction)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncData<Unit>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.use {
|
||||
it.leave().getOrThrow()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.invitelist.impl
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -26,8 +26,8 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
data class InviteListState(
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog,
|
||||
val acceptedAction: Async<RoomId>,
|
||||
val declinedAction: Async<Unit>,
|
||||
val acceptedAction: AsyncData<RoomId>,
|
||||
val declinedAction: AsyncData<Unit>,
|
||||
val eventSink: (InviteListEvents) -> Unit
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.invitelist.impl
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -32,16 +32,16 @@ open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
|||
aInviteListState().copy(inviteList = persistentListOf()),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")),
|
||||
aInviteListState().copy(acceptedAction = Async.Failure(Throwable("Whoops"))),
|
||||
aInviteListState().copy(declinedAction = Async.Failure(Throwable("Whoops"))),
|
||||
aInviteListState().copy(acceptedAction = AsyncData.Failure(Throwable("Whoops"))),
|
||||
aInviteListState().copy(declinedAction = AsyncData.Failure(Throwable("Whoops"))),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aInviteListState() = InviteListState(
|
||||
inviteList = aInviteListInviteSummaryList(),
|
||||
declineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||
acceptedAction = Async.Uninitialized,
|
||||
declinedAction = Async.Uninitialized,
|
||||
acceptedAction = AsyncData.Uninitialized,
|
||||
declinedAction = AsyncData.Uninitialized,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
|
|
@ -56,7 +56,7 @@ fun InviteListView(
|
|||
onInviteAccepted: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.acceptedAction is Async.Success) {
|
||||
if (state.acceptedAction is AsyncData.Success) {
|
||||
LaunchedEffect(state.acceptedAction) {
|
||||
onInviteAccepted(state.acceptedAction.data)
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ fun InviteListView(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.acceptedAction is Async.Failure) {
|
||||
if (state.acceptedAction is AsyncData.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
|
|
@ -98,7 +98,7 @@ fun InviteListView(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.declinedAction is Async.Failure) {
|
||||
if (state.declinedAction is AsyncData.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(CommonStrings.error_unknown),
|
||||
title = stringResource(CommonStrings.common_error),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invitelist.api.SeenInvitesStore
|
||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.MatrixClient
|
||||
|
|
@ -245,7 +245,7 @@ class InviteListPresenterTests {
|
|||
|
||||
val newState = awaitItem()
|
||||
|
||||
assertThat(newState.declinedAction).isEqualTo(Async.Failure<Unit>(ex))
|
||||
assertThat(newState.declinedAction).isEqualTo(AsyncData.Failure<Unit>(ex))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ class InviteListPresenterTests {
|
|||
|
||||
val newState = awaitItem()
|
||||
|
||||
assertThat(newState.declinedAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(newState.declinedAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ class InviteListPresenterTests {
|
|||
|
||||
val newState = awaitItem()
|
||||
|
||||
assertThat(newState.acceptedAction).isEqualTo(Async.Success(A_ROOM_ID))
|
||||
assertThat(newState.acceptedAction).isEqualTo(AsyncData.Success(A_ROOM_ID))
|
||||
assertThat(fakeNotificationDrawerManager.getClearMembershipNotificationForRoomCount(client.sessionId, A_ROOM_ID)).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +323,7 @@ class InviteListPresenterTests {
|
|||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||
|
||||
assertThat(awaitItem().acceptedAction).isEqualTo(Async.Failure<RoomId>(ex))
|
||||
assertThat(awaitItem().acceptedAction).isEqualTo(AsyncData.Failure<RoomId>(ex))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +350,7 @@ class InviteListPresenterTests {
|
|||
originalState.eventSink(InviteListEvents.DismissAcceptError)
|
||||
|
||||
val newState = awaitItem()
|
||||
assertThat(newState.acceptedAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(newState.acceptedAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
|
|||
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
|
|
@ -49,11 +49,11 @@ class PinUnlockPresenter @Inject constructor(
|
|||
@Composable
|
||||
override fun present(): PinUnlockState {
|
||||
val pinEntryState = remember {
|
||||
mutableStateOf<Async<PinEntry>>(Async.Uninitialized)
|
||||
mutableStateOf<AsyncData<PinEntry>>(AsyncData.Uninitialized)
|
||||
}
|
||||
val pinEntry by pinEntryState
|
||||
var remainingAttempts by remember {
|
||||
mutableStateOf<Async<Int>>(Async.Uninitialized)
|
||||
mutableStateOf<AsyncData<Int>>(AsyncData.Uninitialized)
|
||||
}
|
||||
var showWrongPinTitle by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
|
|
@ -62,7 +62,7 @@ class PinUnlockPresenter @Inject constructor(
|
|||
mutableStateOf(false)
|
||||
}
|
||||
val signOutAction = remember {
|
||||
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
||||
mutableStateOf<AsyncData<String?>>(AsyncData.Uninitialized)
|
||||
}
|
||||
var biometricUnlockResult by remember {
|
||||
mutableStateOf<BiometricUnlock.AuthenticationResult?>(null)
|
||||
|
|
@ -91,7 +91,7 @@ class PinUnlockPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber()
|
||||
remainingAttempts = Async.Success(remainingAttemptsNumber)
|
||||
remainingAttempts = AsyncData.Success(remainingAttemptsNumber)
|
||||
if (remainingAttemptsNumber == 0) {
|
||||
showSignOutPrompt = true
|
||||
}
|
||||
|
|
@ -139,46 +139,46 @@ class PinUnlockPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.isComplete(): Boolean {
|
||||
private fun AsyncData<PinEntry>.isComplete(): Boolean {
|
||||
return dataOrNull()?.isComplete().orFalse()
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.toText(): String {
|
||||
private fun AsyncData<PinEntry>.toText(): String {
|
||||
return dataOrNull()?.toText() ?: ""
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.clear(): Async<PinEntry> {
|
||||
private fun AsyncData<PinEntry>.clear(): AsyncData<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> Async.Success(data.clear())
|
||||
is AsyncData.Success -> AsyncData.Success(data.clear())
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> {
|
||||
private fun AsyncData<PinEntry>.process(pinKeypadModel: PinKeypadModel): AsyncData<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> {
|
||||
is AsyncData.Success -> {
|
||||
val pinEntry = when (pinKeypadModel) {
|
||||
PinKeypadModel.Back -> data.deleteLast()
|
||||
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
|
||||
PinKeypadModel.Empty -> data
|
||||
}
|
||||
Async.Success(pinEntry)
|
||||
AsyncData.Success(pinEntry)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.process(pinEntryAsText: String): Async<PinEntry> {
|
||||
private fun AsyncData<PinEntry>.process(pinEntryAsText: String): AsyncData<PinEntry> {
|
||||
return when (this) {
|
||||
is Async.Success -> {
|
||||
is AsyncData.Success -> {
|
||||
val pinEntry = data.fillWith(pinEntryAsText)
|
||||
Async.Success(pinEntry)
|
||||
AsyncData.Success(pinEntry)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
||||
suspend {
|
||||
matrixClient.logout(ignoreSdkError = true)
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
|
|
|
|||
|
|
@ -19,21 +19,21 @@ package io.element.android.features.lockscreen.impl.unlock
|
|||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockError
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class PinUnlockState(
|
||||
val pinEntry: Async<PinEntry>,
|
||||
val pinEntry: AsyncData<PinEntry>,
|
||||
val showWrongPinTitle: Boolean,
|
||||
val remainingAttempts: Async<Int>,
|
||||
val remainingAttempts: AsyncData<Int>,
|
||||
val showSignOutPrompt: Boolean,
|
||||
val signOutAction: Async<String?>,
|
||||
val signOutAction: AsyncData<String?>,
|
||||
val showBiometricUnlock: Boolean,
|
||||
val isUnlocked: Boolean,
|
||||
val biometricUnlockResult: BiometricUnlock.AuthenticationResult?,
|
||||
val eventSink: (PinUnlockEvents) -> Unit
|
||||
) {
|
||||
val isSignOutPromptCancellable = when (remainingAttempts) {
|
||||
is Async.Success -> remainingAttempts.data > 0
|
||||
is AsyncData.Success -> remainingAttempts.data > 0
|
||||
else -> true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.lockscreen.impl.unlock
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlock
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||
override val values: Sequence<PinUnlockState>
|
||||
|
|
@ -30,7 +30,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
|||
aPinUnlockState(showSignOutPrompt = true),
|
||||
aPinUnlockState(showBiometricUnlock = false),
|
||||
aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0),
|
||||
aPinUnlockState(signOutAction = Async.Loading()),
|
||||
aPinUnlockState(signOutAction = AsyncData.Loading()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,11 +42,11 @@ fun aPinUnlockState(
|
|||
showBiometricUnlock: Boolean = true,
|
||||
biometricUnlockResult: BiometricUnlock.AuthenticationResult? = null,
|
||||
isUnlocked: Boolean = false,
|
||||
signOutAction: Async<String?> = Async.Uninitialized,
|
||||
signOutAction: AsyncData<String?> = AsyncData.Uninitialized,
|
||||
) = PinUnlockState(
|
||||
pinEntry = Async.Success(pinEntry),
|
||||
pinEntry = AsyncData.Success(pinEntry),
|
||||
showWrongPinTitle = showWrongPinTitle,
|
||||
remainingAttempts = Async.Success(remainingAttempts),
|
||||
remainingAttempts = AsyncData.Success(remainingAttempts),
|
||||
showSignOutPrompt = showSignOutPrompt,
|
||||
showBiometricUnlock = showBiometricUnlock,
|
||||
signOutAction = signOutAction,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import io.element.android.features.lockscreen.impl.components.PinEntryTextField
|
|||
import io.element.android.features.lockscreen.impl.pin.model.PinDigit
|
||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
|
|
@ -92,7 +92,7 @@ fun PinUnlockView(
|
|||
onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) },
|
||||
)
|
||||
}
|
||||
if (state.signOutAction is Async.Loading) {
|
||||
if (state.signOutAction is AsyncData.Loading) {
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
}
|
||||
if (state.showBiometricUnlockError) {
|
||||
|
|
@ -335,7 +335,7 @@ private fun PinUnlockHeader(
|
|||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = subtitleColor,
|
||||
)
|
||||
if (!isInAppUnlock && state.pinEntry is Async.Success) {
|
||||
if (!isInAppUnlock && state.pinEntry is AsyncData.Success) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
PinDotsRow(state.pinEntry.data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<string name="screen_app_lock_settings_remove_pin_alert_title">"PIN entfernen?"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"%1$s zulassen"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Ich möchte diese PIN verwenden."</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App jedes Mal zu entsperren"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Spare dir etwas Zeit und benutze %1$s, um die App zu entsperren"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"PIN wählen"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
|
||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
|||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
|
|
@ -48,15 +48,15 @@ class PinUnlockPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(state.pinEntry).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
assertThat(state.showWrongPinTitle).isFalse()
|
||||
assertThat(state.showSignOutPrompt).isFalse()
|
||||
assertThat(state.isUnlocked).isFalse()
|
||||
assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
consumeItemsUntilPredicate {
|
||||
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
|
||||
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
|
||||
}.last().also { state ->
|
||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1')))
|
||||
state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2')))
|
||||
|
|
@ -83,7 +83,7 @@ class PinUnlockPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = consumeItemsUntilPredicate {
|
||||
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
|
||||
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
|
||||
}.last()
|
||||
val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0
|
||||
repeat(numberOfAttempts) {
|
||||
|
|
@ -107,7 +107,7 @@ class PinUnlockPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
consumeItemsUntilPredicate {
|
||||
it.pinEntry is Async.Success && it.remainingAttempts is Async.Success
|
||||
it.pinEntry is AsyncData.Success && it.remainingAttempts is AsyncData.Success
|
||||
}.last().also { state ->
|
||||
state.eventSink(PinUnlockEvents.OnForgetPin)
|
||||
}
|
||||
|
|
@ -125,12 +125,12 @@ class PinUnlockPresenterTest {
|
|||
state.eventSink(PinUnlockEvents.SignOut)
|
||||
}
|
||||
consumeItemsUntilPredicate { state ->
|
||||
state.signOutAction is Async.Success
|
||||
state.signOutAction is AsyncData.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Async<PinEntry>.assertText(text: String) {
|
||||
private fun AsyncData<PinEntry>.assertText(text: String) {
|
||||
dataOrNull()?.assertText(text)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
|
|
@ -41,14 +41,14 @@ class ChangeServerPresenter @Inject constructor(
|
|||
override fun present(): ChangeServerState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val changeServerAction: MutableState<Async<Unit>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val changeServerAction: MutableState<AsyncData<Unit>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
fun handleEvents(event: ChangeServerEvents) {
|
||||
when (event) {
|
||||
is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction)
|
||||
ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized
|
||||
ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ class ChangeServerPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.changeServer(
|
||||
data: AccountProvider,
|
||||
changeServerAction: MutableState<Async<Unit>>,
|
||||
changeServerAction: MutableState<AsyncData<Unit>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(data.url).map {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package io.element.android.features.login.impl.changeserver
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class ChangeServerState(
|
||||
val changeServerAction: Async<Unit>,
|
||||
val changeServerAction: AsyncData<Unit>,
|
||||
val eventSink: (ChangeServerEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.login.impl.changeserver
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerState> {
|
||||
override val values: Sequence<ChangeServerState>
|
||||
|
|
@ -27,6 +27,6 @@ open class ChangeServerStateProvider : PreviewParameterProvider<ChangeServerStat
|
|||
}
|
||||
|
||||
fun aChangeServerState() = ChangeServerState(
|
||||
changeServerAction = Async.Uninitialized,
|
||||
changeServerAction = AsyncData.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -37,7 +37,7 @@ fun ChangeServerView(
|
|||
) {
|
||||
val eventSink = state.eventSink
|
||||
when (state.changeServerAction) {
|
||||
is Async.Failure -> {
|
||||
is AsyncData.Failure -> {
|
||||
when (val error = state.changeServerAction.error) {
|
||||
is ChangeServerError.Error -> {
|
||||
ErrorDialog(
|
||||
|
|
@ -60,11 +60,11 @@ fun ChangeServerView(
|
|||
}
|
||||
}
|
||||
}
|
||||
is Async.Loading -> ProgressDialog()
|
||||
is Async.Success -> LaunchedEffect(state.changeServerAction) {
|
||||
is AsyncData.Loading -> ProgressDialog()
|
||||
is AsyncData.Success -> LaunchedEffect(state.changeServerAction) {
|
||||
onDone()
|
||||
}
|
||||
Async.Uninitialized -> Unit
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
|
@ -44,33 +44,33 @@ class OidcPresenter @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): OidcState {
|
||||
var requestState: Async<Unit> by remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
var requestState: AsyncAction<Unit> by remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun handleCancel() {
|
||||
requestState = Async.Loading()
|
||||
requestState = AsyncAction.Loading
|
||||
localCoroutineScope.launch {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.fold(
|
||||
onSuccess = {
|
||||
// Then go back
|
||||
requestState = Async.Success(Unit)
|
||||
requestState = AsyncAction.Success(Unit)
|
||||
},
|
||||
onFailure = {
|
||||
requestState = Async.Failure(it)
|
||||
requestState = AsyncAction.Failure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSuccess(url: String) {
|
||||
requestState = Async.Loading()
|
||||
requestState = AsyncAction.Loading
|
||||
localCoroutineScope.launch {
|
||||
authenticationService.loginWithOidc(url)
|
||||
.onFailure {
|
||||
requestState = Async.Failure(it)
|
||||
requestState = AsyncAction.Failure(it)
|
||||
}
|
||||
// On success, the node tree will be updated, there is nothing to do
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ class OidcPresenter @AssistedInject constructor(
|
|||
when (event) {
|
||||
OidcEvents.Cancel -> handleCancel()
|
||||
is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction)
|
||||
OidcEvents.ClearError -> requestState = Async.Uninitialized
|
||||
OidcEvents.ClearError -> requestState = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
package io.element.android.features.login.impl.oidc.webview
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
data class OidcState(
|
||||
val oidcDetails: OidcDetails,
|
||||
val requestState: Async<Unit>,
|
||||
val requestState: AsyncAction<Unit>,
|
||||
val eventSink: (OidcEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@
|
|||
package io.element.android.features.login.impl.oidc.webview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
open class OidcStateProvider : PreviewParameterProvider<OidcState> {
|
||||
override val values: Sequence<OidcState>
|
||||
get() = sequenceOf(
|
||||
aOidcState(),
|
||||
aOidcState().copy(requestState = Async.Loading()),
|
||||
aOidcState().copy(requestState = AsyncAction.Loading),
|
||||
)
|
||||
}
|
||||
|
||||
fun aOidcState() = OidcState(
|
||||
oidcDetails = aOidcDetails(),
|
||||
requestState = Async.Uninitialized,
|
||||
requestState = AsyncAction.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import io.element.android.features.login.impl.oidc.OidcUrlParser
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncView
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ fun OidcView(
|
|||
}
|
||||
)
|
||||
|
||||
AsyncView(
|
||||
AsyncActionView(
|
||||
async = state.requestState,
|
||||
onSuccess = { onNavigateBack() },
|
||||
onErrorDismiss = { state.eventSink(OidcEvents.ClearError) }
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import io.element.android.features.login.impl.DefaultLoginUserStory
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
|
|
@ -61,8 +61,8 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
val accountProvider by accountProviderDataSource.flow().collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val loginFlowAction: MutableState<Async<LoginFlow>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val loginFlowAction: MutableState<AsyncData<LoginFlow>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
@ -78,7 +78,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
ConfirmAccountProviderEvents.Continue -> {
|
||||
localCoroutineScope.submit(accountProvider.url, loginFlowAction)
|
||||
}
|
||||
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized
|
||||
ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.submit(
|
||||
homeserverUrl: String,
|
||||
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
|
||||
) = launch {
|
||||
suspend {
|
||||
authenticationService.setHomeserver(homeserverUrl).map {
|
||||
|
|
@ -111,17 +111,17 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun onOidcAction(
|
||||
oidcAction: OidcAction,
|
||||
loginFlowAction: MutableState<Async<LoginFlow>>,
|
||||
loginFlowAction: MutableState<AsyncData<LoginFlow>>,
|
||||
) {
|
||||
loginFlowAction.value = Async.Loading()
|
||||
loginFlowAction.value = AsyncData.Loading()
|
||||
when (oidcAction) {
|
||||
OidcAction.GoBack -> {
|
||||
authenticationService.cancelOidcLogin()
|
||||
.onSuccess {
|
||||
loginFlowAction.value = Async.Uninitialized
|
||||
loginFlowAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = Async.Failure(failure)
|
||||
loginFlowAction.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
is OidcAction.Success -> {
|
||||
|
|
@ -130,7 +130,7 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor(
|
|||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loginFlowAction.value = Async.Failure(failure)
|
||||
loginFlowAction.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,17 +17,17 @@
|
|||
package io.element.android.features.login.impl.screens.confirmaccountprovider
|
||||
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.auth.OidcDetails
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class ConfirmAccountProviderState(
|
||||
val accountProvider: AccountProvider,
|
||||
val isAccountCreation: Boolean,
|
||||
val loginFlow: Async<LoginFlow>,
|
||||
val loginFlow: AsyncData<LoginFlow>,
|
||||
val eventSink: (ConfirmAccountProviderEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading)
|
||||
val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginFlow is AsyncData.Uninitialized || loginFlow is AsyncData.Loading)
|
||||
}
|
||||
|
||||
sealed interface LoginFlow {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<ConfirmAccountProviderState> {
|
||||
override val values: Sequence<ConfirmAccountProviderState>
|
||||
|
|
@ -31,6 +31,6 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider<Confir
|
|||
fun aConfirmAccountProviderState() = ConfirmAccountProviderState(
|
||||
accountProvider = anAccountProvider(),
|
||||
isAccountCreation = false,
|
||||
loginFlow = Async.Uninitialized,
|
||||
loginFlow = AsyncData.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog
|
||||
import io.element.android.features.login.impl.error.ChangeServerError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
|
|
@ -56,7 +56,7 @@ fun ConfirmAccountProviderView(
|
|||
) {
|
||||
val isLoading by remember(state.loginFlow) {
|
||||
derivedStateOf {
|
||||
state.loginFlow is Async.Loading
|
||||
state.loginFlow is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val eventSink = state.eventSink
|
||||
|
|
@ -107,7 +107,7 @@ fun ConfirmAccountProviderView(
|
|||
}
|
||||
) {
|
||||
when (state.loginFlow) {
|
||||
is Async.Failure -> {
|
||||
is AsyncData.Failure -> {
|
||||
when (val error = state.loginFlow.error) {
|
||||
is ChangeServerError.Error -> {
|
||||
ErrorDialog(
|
||||
|
|
@ -127,14 +127,14 @@ fun ConfirmAccountProviderView(
|
|||
}
|
||||
}
|
||||
}
|
||||
is Async.Loading -> Unit // The Continue button shows the loading state
|
||||
is Async.Success -> {
|
||||
is AsyncData.Loading -> Unit // The Continue button shows the loading state
|
||||
is AsyncData.Success -> {
|
||||
when (val loginFlowState = state.loginFlow.data) {
|
||||
is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails)
|
||||
LoginFlow.PasswordLogin -> onLoginPasswordNeeded()
|
||||
}
|
||||
}
|
||||
Async.Uninitialized -> Unit
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -43,8 +43,8 @@ class LoginPasswordPresenter @Inject constructor(
|
|||
@Composable
|
||||
override fun present(): LoginPasswordState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val loginAction: MutableState<Async<SessionId>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val loginAction: MutableState<AsyncData<SessionId>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
val formState = rememberSaveable {
|
||||
|
|
@ -63,7 +63,7 @@ class LoginPasswordPresenter @Inject constructor(
|
|||
LoginPasswordEvents.Submit -> {
|
||||
localCoroutineScope.submit(formState.value, loginAction)
|
||||
}
|
||||
LoginPasswordEvents.ClearError -> loginAction.value = Async.Uninitialized
|
||||
LoginPasswordEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,16 +75,16 @@ class LoginPasswordPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState<Async<SessionId>>) = launch {
|
||||
loggedInState.value = Async.Loading()
|
||||
private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
|
||||
loggedInState.value = AsyncData.Loading()
|
||||
authenticationService.login(formState.login.trim(), formState.password)
|
||||
.onSuccess { sessionId ->
|
||||
// We will not navigate to the WaitList screen, so the login user story is done
|
||||
defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
loggedInState.value = Async.Success(sessionId)
|
||||
loggedInState.value = AsyncData.Success(sessionId)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loggedInState.value = Async.Failure(failure)
|
||||
loggedInState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ package io.element.android.features.login.impl.screens.loginpassword
|
|||
|
||||
import android.os.Parcelable
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class LoginPasswordState(
|
||||
val accountProvider: AccountProvider,
|
||||
val formState: LoginFormState,
|
||||
val loginAction: Async<SessionId>,
|
||||
val loginAction: AsyncData<SessionId>,
|
||||
val eventSink: (LoginPasswordEvents) -> Unit
|
||||
) {
|
||||
val submitEnabled: Boolean
|
||||
get() = loginAction !is Async.Failure &&
|
||||
get() = loginAction !is AsyncData.Failure &&
|
||||
formState.login.isNotEmpty() &&
|
||||
formState.password.isNotEmpty()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,22 +18,22 @@ package io.element.android.features.login.impl.screens.loginpassword
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.login.impl.accountprovider.anAccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {
|
||||
override val values: Sequence<LoginPasswordState>
|
||||
get() = sequenceOf(
|
||||
aLoginPasswordState(),
|
||||
// Loading
|
||||
aLoginPasswordState().copy(loginAction = Async.Loading()),
|
||||
aLoginPasswordState().copy(loginAction = AsyncData.Loading()),
|
||||
// Error
|
||||
aLoginPasswordState().copy(loginAction = Async.Failure(Exception("An error occurred"))),
|
||||
aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))),
|
||||
)
|
||||
}
|
||||
|
||||
fun aLoginPasswordState() = LoginPasswordState(
|
||||
accountProvider = anAccountProvider(),
|
||||
formState = LoginFormState.Default,
|
||||
loginAction = Async.Uninitialized,
|
||||
loginAction = AsyncData.Uninitialized,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.error.isWaitListError
|
||||
import io.element.android.features.login.impl.error.loginError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
|
|
@ -84,7 +84,7 @@ fun LoginPasswordView(
|
|||
) {
|
||||
val isLoading by remember(state.loginAction) {
|
||||
derivedStateOf {
|
||||
state.loginAction is Async.Loading
|
||||
state.loginAction is AsyncData.Loading
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
|
@ -148,7 +148,7 @@ fun LoginPasswordView(
|
|||
)
|
||||
Spacer(modifier = Modifier.height(60.dp))
|
||||
|
||||
if (state.loginAction is Async.Failure) {
|
||||
if (state.loginAction is AsyncData.Failure) {
|
||||
when {
|
||||
state.loginAction.error.isWaitListError() -> {
|
||||
onWaitListError(state.formState)
|
||||
|
|
@ -224,7 +224,7 @@ private fun LoginForm(
|
|||
)
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
if (state.loginAction is Async.Loading) {
|
||||
if (state.loginAction is AsyncData.Loading) {
|
||||
// Ensure password is hidden when user submits the form
|
||||
passwordVisible = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
|
|||
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
|
||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -46,8 +46,8 @@ class SearchAccountProviderPresenter @Inject constructor(
|
|||
}
|
||||
val changeServerState = changeServerPresenter.present()
|
||||
|
||||
val data: MutableState<Async<List<HomeserverData>>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val data: MutableState<AsyncData<List<HomeserverData>>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(userInput) {
|
||||
|
|
@ -70,16 +70,16 @@ class SearchAccountProviderPresenter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<Async<List<HomeserverData>>>) = launch {
|
||||
data.value = Async.Uninitialized
|
||||
private fun CoroutineScope.onUserInput(userInput: String, data: MutableState<AsyncData<List<HomeserverData>>>) = launch {
|
||||
data.value = AsyncData.Uninitialized
|
||||
// Debounce
|
||||
delay(300)
|
||||
data.value = Async.Loading()
|
||||
data.value = AsyncData.Loading()
|
||||
homeserverResolver.resolve(userInput).collect {
|
||||
data.value = Async.Success(it)
|
||||
data.value = AsyncData.Success(it)
|
||||
}
|
||||
if (data.value !is Async.Success) {
|
||||
data.value = Async.Uninitialized
|
||||
if (data.value !is AsyncData.Success) {
|
||||
data.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ package io.element.android.features.login.impl.screens.searchaccountprovider
|
|||
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerState
|
||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class SearchAccountProviderState(
|
||||
val userInput: String,
|
||||
val userInputResult: Async<List<HomeserverData>>,
|
||||
val userInputResult: AsyncData<List<HomeserverData>>,
|
||||
val changeServerState: ChangeServerState,
|
||||
val eventSink: (SearchAccountProviderEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,20 +20,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import io.element.android.appconfig.AuthenticationConfig
|
||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
open class SearchAccountProviderStateProvider : PreviewParameterProvider<SearchAccountProviderState> {
|
||||
override val values: Sequence<SearchAccountProviderState>
|
||||
get() = sequenceOf(
|
||||
aSearchAccountProviderState(),
|
||||
aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())),
|
||||
aSearchAccountProviderState(userInputResult = AsyncData.Success(aHomeserverDataList())),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
||||
fun aSearchAccountProviderState(
|
||||
userInput: String = "",
|
||||
userInputResult: Async<List<HomeserverData>> = Async.Uninitialized,
|
||||
userInputResult: AsyncData<List<HomeserverData>> = AsyncData.Uninitialized,
|
||||
) = SearchAccountProviderState(
|
||||
userInput = userInput,
|
||||
userInputResult = userInputResult,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderVie
|
|||
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
|
||||
import io.element.android.features.login.impl.changeserver.ChangeServerView
|
||||
import io.element.android.features.login.impl.resolver.HomeserverData
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.form.textFieldState
|
||||
|
|
@ -152,10 +152,10 @@ fun SearchAccountProviderView(
|
|||
}
|
||||
|
||||
when (state.userInputResult) {
|
||||
is Async.Failure -> {
|
||||
is AsyncData.Failure -> {
|
||||
// Ignore errors (let the user type more chars)
|
||||
}
|
||||
is Async.Loading -> {
|
||||
is AsyncData.Loading -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -167,7 +167,7 @@ fun SearchAccountProviderView(
|
|||
}
|
||||
}
|
||||
}
|
||||
is Async.Success -> {
|
||||
is AsyncData.Success -> {
|
||||
items(state.userInputResult.data) { homeserverData ->
|
||||
val item = homeserverData.toAccountProvider()
|
||||
AccountProviderView(
|
||||
|
|
@ -178,7 +178,7 @@ fun SearchAccountProviderView(
|
|||
)
|
||||
}
|
||||
}
|
||||
Async.Uninitialized -> Unit
|
||||
AsyncData.Uninitialized -> Unit
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
|
|
@ -55,8 +55,8 @@ class WaitListPresenter @AssistedInject constructor(
|
|||
authenticationService.getHomeserverDetails().value?.url ?: "server"
|
||||
}
|
||||
|
||||
val loginAction: MutableState<Async<SessionId>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val loginAction: MutableState<AsyncData<SessionId>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
val attemptNumber = remember { mutableIntStateOf(0) }
|
||||
|
|
@ -70,7 +70,7 @@ class WaitListPresenter @AssistedInject constructor(
|
|||
coroutineScope.loginAttempt(formState, loginAction)
|
||||
}
|
||||
}
|
||||
WaitListEvents.ClearError -> loginAction.value = Async.Uninitialized
|
||||
WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized
|
||||
WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -83,15 +83,15 @@ class WaitListPresenter @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<Async<SessionId>>) = launch {
|
||||
private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState<AsyncData<SessionId>>) = launch {
|
||||
Timber.w("Attempt to login...")
|
||||
loggedInState.value = Async.Loading()
|
||||
loggedInState.value = AsyncData.Loading()
|
||||
authenticationService.login(formState.login.trim(), formState.password)
|
||||
.onSuccess { sessionId ->
|
||||
loggedInState.value = Async.Success(sessionId)
|
||||
loggedInState.value = AsyncData.Success(sessionId)
|
||||
}
|
||||
.onFailure { failure ->
|
||||
loggedInState.value = Async.Failure(failure)
|
||||
loggedInState.value = AsyncData.Failure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
// Do not use default value, so no member get forgotten in the presenters.
|
||||
data class WaitListState(
|
||||
val appName: String,
|
||||
val serverName: String,
|
||||
val loginAction: Async<SessionId>,
|
||||
val loginAction: AsyncData<SessionId>,
|
||||
val eventSink: (WaitListEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,17 +17,17 @@
|
|||
package io.element.android.features.login.impl.screens.waitlistscreen
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
|
||||
override val values: Sequence<WaitListState>
|
||||
get() = sequenceOf(
|
||||
aWaitListState(loginAction = Async.Uninitialized),
|
||||
aWaitListState(loginAction = Async.Loading()),
|
||||
aWaitListState(loginAction = Async.Failure(Throwable("error"))),
|
||||
aWaitListState(loginAction = Async.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
|
||||
aWaitListState(loginAction = Async.Success(SessionId("@alice:element.io"))),
|
||||
aWaitListState(loginAction = AsyncData.Uninitialized),
|
||||
aWaitListState(loginAction = AsyncData.Loading()),
|
||||
aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))),
|
||||
aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))),
|
||||
aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))),
|
||||
// Add other state here
|
||||
)
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ open class WaitListStateProvider : PreviewParameterProvider<WaitListState> {
|
|||
fun aWaitListState(
|
||||
appName: String = "Element X",
|
||||
serverName: String = "server.org",
|
||||
loginAction: Async<SessionId> = Async.Uninitialized,
|
||||
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
|
||||
) = WaitListState(
|
||||
appName = appName,
|
||||
serverName = serverName,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import androidx.lifecycle.Lifecycle
|
|||
import io.element.android.features.login.impl.R
|
||||
import io.element.android.features.login.impl.error.isWaitListError
|
||||
import io.element.android.features.login.impl.error.loginError
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.pages.SunsetPage
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -89,12 +89,12 @@ private fun WaitListContent(
|
|||
) {
|
||||
val title = stringResource(
|
||||
when (state.loginAction) {
|
||||
is Async.Success -> R.string.screen_waitlist_title_success
|
||||
is AsyncData.Success -> R.string.screen_waitlist_title_success
|
||||
else -> R.string.screen_waitlist_title
|
||||
}
|
||||
)
|
||||
val subtitle = when (state.loginAction) {
|
||||
is Async.Success -> stringResource(
|
||||
is AsyncData.Success -> stringResource(
|
||||
id = R.string.screen_waitlist_message_success,
|
||||
state.appName,
|
||||
)
|
||||
|
|
@ -122,7 +122,7 @@ private fun OverallContent(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (state.loginAction !is Async.Success) {
|
||||
if (state.loginAction !is AsyncData.Success) {
|
||||
CompositionLocalProvider(LocalContentColor provides Color.Black) {
|
||||
TextButton(
|
||||
text = stringResource(CommonStrings.action_cancel),
|
||||
|
|
@ -130,7 +130,7 @@ private fun OverallContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
if (state.loginAction is Async.Success) {
|
||||
if (state.loginAction is AsyncData.Success) {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
onClick = { state.eventSink.invoke(WaitListEvents.Continue) },
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
|
|
@ -46,7 +46,7 @@ class ChangeServerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,13 +61,13 @@ class ChangeServerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||
authenticationService.givenHomeserver(A_HOMESERVER)
|
||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.changeServerAction).isEqualTo(Async.Success(Unit))
|
||||
assertThat(successState.changeServerAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,16 +82,16 @@ class ChangeServerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(failureState.changeServerAction).isInstanceOf(AsyncData.Failure::class.java)
|
||||
// Clear error
|
||||
failureState.eventSink.invoke(ChangeServerEvents.ClearError)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.changeServerAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.api.oidc.OidcAction
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
|
|
@ -49,7 +49,7 @@ class OidcPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA)
|
||||
assertThat(initialState.requestState).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.requestState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +65,9 @@ class OidcPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(OidcEvents.Cancel)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.requestState).isEqualTo(Async.Success(Unit))
|
||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,9 +85,9 @@ class OidcPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(OidcEvents.Cancel)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.requestState).isEqualTo(Async.Failure<Unit>(A_THROWABLE))
|
||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
// Note: in real life I do not think this can happen, and the app should not block the user.
|
||||
}
|
||||
}
|
||||
|
|
@ -104,9 +104,9 @@ class OidcPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.requestState).isEqualTo(Async.Success(Unit))
|
||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ class OidcPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
||||
// In this case, no success, the session is created and the node get destroyed.
|
||||
}
|
||||
}
|
||||
|
|
@ -141,12 +141,12 @@ class OidcPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.requestState).isEqualTo(Async.Loading<Unit>())
|
||||
assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.requestState).isEqualTo(Async.Failure<Unit>(A_THROWABLE))
|
||||
assertThat(errorState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
errorState.eventSink.invoke(OidcEvents.ClearError)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.requestState).isEqualTo(Async.Uninitialized)
|
||||
assertThat(finalState.requestState).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import io.element.android.features.login.impl.DefaultLoginUserStory
|
|||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
|
||||
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
|
||||
|
|
@ -52,7 +52,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
assertThat(initialState.isAccountCreation).isFalse()
|
||||
assertThat(initialState.submitEnabled).isTrue()
|
||||
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
|
||||
assertThat(initialState.loginFlow).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +70,10 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin)
|
||||
}
|
||||
}
|
||||
|
|
@ -92,10 +92,10 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -116,15 +116,15 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||
authenticationService.givenOidcCancelError(A_THROWABLE)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack)
|
||||
val cancelFailureState = awaitItem()
|
||||
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,14 +144,14 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||
defaultOidcActionFlow.post(OidcAction.GoBack)
|
||||
val cancelFinalState = awaitItem()
|
||||
assertThat(cancelFinalState.loginFlow).isInstanceOf(Async.Uninitialized::class.java)
|
||||
assertThat(cancelFinalState.loginFlow).isInstanceOf(AsyncData.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,17 +171,17 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||
authenticationService.givenLoginError(A_THROWABLE)
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
val cancelLoadingState = awaitItem()
|
||||
assertThat(cancelLoadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(cancelLoadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val cancelFailureState = awaitItem()
|
||||
assertThat(cancelFailureState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(cancelFailureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,15 +205,15 @@ class ConfirmAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.submitEnabled).isTrue()
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.submitEnabled).isFalse()
|
||||
assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.loginFlow).isInstanceOf(AsyncData.Success::class.java)
|
||||
assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java)
|
||||
assertThat(defaultLoginUserStory.loginFlowIsDone.value).isFalse()
|
||||
defaultOidcActionFlow.post(OidcAction.Success("aUrl"))
|
||||
val successSuccessState = awaitItem()
|
||||
assertThat(successSuccessState.loginFlow).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(successSuccessState.loginFlow).isInstanceOf(AsyncData.Loading::class.java)
|
||||
waitForPredicate { defaultLoginUserStory.loginFlowIsDone.value }
|
||||
}
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ class ConfirmAccountProviderPresenterTest {
|
|||
skipItems(1) // Loading
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.submitEnabled).isFalse()
|
||||
assertThat(failureState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(failureState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,12 +256,12 @@ class ConfirmAccountProviderPresenterTest {
|
|||
|
||||
// Check an error was returned
|
||||
val submittedState = awaitItem()
|
||||
assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java)
|
||||
assertThat(submittedState.loginFlow).isInstanceOf(AsyncData.Failure::class.java)
|
||||
|
||||
// Assert the error is then cleared
|
||||
submittedState.eventSink(ConfirmAccountProviderEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized)
|
||||
assertThat(clearedState.loginFlow).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||
import io.element.android.features.login.impl.util.defaultAccountProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_PASSWORD
|
||||
|
|
@ -57,7 +57,7 @@ class LoginPasswordPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider)
|
||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
|
||||
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(initialState.submitEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -110,9 +110,9 @@ class LoginPasswordPresenterTest {
|
|||
val loginAndPasswordState = awaitItem()
|
||||
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||
val submitState = awaitItem()
|
||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val loggedInState = awaitItem()
|
||||
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID))
|
||||
assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Success(A_SESSION_ID))
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -139,9 +139,9 @@ class LoginPasswordPresenterTest {
|
|||
authenticationService.givenLoginError(A_THROWABLE)
|
||||
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||
val submitState = awaitItem()
|
||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val loggedInState = awaitItem()
|
||||
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||
assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,14 +167,14 @@ class LoginPasswordPresenterTest {
|
|||
authenticationService.givenLoginError(A_THROWABLE)
|
||||
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit)
|
||||
val submitState = awaitItem()
|
||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val loggedInState = awaitItem()
|
||||
// Check an error was returned
|
||||
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||
assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
|
||||
// Assert the error is then cleared
|
||||
loggedInState.eventSink(LoginPasswordEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import io.element.android.features.login.impl.resolver.network.FakeWellknownRequ
|
|||
import io.element.android.features.login.impl.resolver.network.WellKnown
|
||||
import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig
|
||||
import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -57,7 +57,7 @@ class SearchAccountProviderPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userInput).isEmpty()
|
||||
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +79,9 @@ class SearchAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("test")
|
||||
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,10 +103,10 @@ class SearchAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("https://test.org")
|
||||
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||
Async.Success(
|
||||
AsyncData.Success(
|
||||
listOf(
|
||||
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false)
|
||||
)
|
||||
|
|
@ -138,10 +138,10 @@ class SearchAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("test")
|
||||
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||
Async.Success(
|
||||
AsyncData.Success(
|
||||
listOf(
|
||||
aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false)
|
||||
)
|
||||
|
|
@ -173,10 +173,10 @@ class SearchAccountProviderPresenterTest {
|
|||
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
|
||||
val withInputState = awaitItem()
|
||||
assertThat(withInputState.userInput).isEqualTo("test")
|
||||
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||
Async.Success(
|
||||
AsyncData.Success(
|
||||
listOf(
|
||||
aHomeserverData(homeserverUrl = "https://test.io")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||
import io.element.android.features.login.impl.screens.loginpassword.LoginFormState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||
|
|
@ -58,7 +58,7 @@ class WaitListPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.appName).isEqualTo("Application Name")
|
||||
assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL)
|
||||
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,13 +83,13 @@ class WaitListPresenterTest {
|
|||
expectNoEvents()
|
||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
||||
val submitState = awaitItem()
|
||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE))
|
||||
assertThat(errorState.loginAction).isEqualTo(AsyncData.Failure<SessionId>(A_THROWABLE))
|
||||
// Assert the error can be cleared
|
||||
errorState.eventSink(WaitListEvents.ClearError)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,9 +113,9 @@ class WaitListPresenterTest {
|
|||
expectNoEvents()
|
||||
initialState.eventSink.invoke(WaitListEvents.AttemptLogin)
|
||||
val submitState = awaitItem()
|
||||
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.loginAction).isEqualTo(Async.Success(A_USER_ID))
|
||||
assertThat(successState.loginAction).isEqualTo(AsyncData.Success(A_USER_ID))
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
|
||||
successState.eventSink.invoke(WaitListEvents.Continue)
|
||||
assertThat(loginUserStory.loginFlowIsDone.value).isTrue()
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@
|
|||
|
||||
package io.element.android.features.logout.api.direct
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class DirectLogoutState(
|
||||
val canDoDirectSignOut: Boolean,
|
||||
val showConfirmationDialog: Boolean,
|
||||
val logoutAction: Async<String?>,
|
||||
val logoutAction: AsyncAction<String?>,
|
||||
val eventSink: (DirectLogoutEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
|
|
@ -50,8 +51,8 @@ class LogoutPresenter @Inject constructor(
|
|||
@Composable
|
||||
override fun present(): LogoutState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val logoutAction: MutableState<Async<String?>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val logoutAction: MutableState<AsyncAction<String?>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
|
|
@ -66,7 +67,6 @@ class LogoutPresenter @Inject constructor(
|
|||
}
|
||||
.collectAsState(initial = BackupUploadState.Unknown)
|
||||
|
||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||
var isLastSession by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
|
||||
|
|
@ -75,8 +75,8 @@ class LogoutPresenter @Inject constructor(
|
|||
val backupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
|
||||
val doesBackupExistOnServerAction: MutableState<Async<Boolean>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val doesBackupExistOnServerAction: MutableState<AsyncData<Boolean>> = remember {
|
||||
mutableStateOf(AsyncData.Uninitialized)
|
||||
}
|
||||
|
||||
LaunchedEffect(backupState) {
|
||||
|
|
@ -88,16 +88,14 @@ class LogoutPresenter @Inject constructor(
|
|||
fun handleEvents(event: LogoutEvents) {
|
||||
when (event) {
|
||||
is LogoutEvents.Logout -> {
|
||||
if (showLogoutDialog || event.ignoreSdkError) {
|
||||
showLogoutDialog = false
|
||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||
} else {
|
||||
showLogoutDialog = true
|
||||
logoutAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
LogoutEvents.CloseDialogs -> {
|
||||
logoutAction.value = Async.Uninitialized
|
||||
showLogoutDialog = false
|
||||
logoutAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,20 +106,19 @@ class LogoutPresenter @Inject constructor(
|
|||
doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(),
|
||||
recoveryState = recoveryState,
|
||||
backupUploadState = backupUploadState,
|
||||
showConfirmationDialog = showLogoutDialog,
|
||||
logoutAction = logoutAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.getKeyBackupStatus(action: MutableState<Async<Boolean>>) = launch {
|
||||
private fun CoroutineScope.getKeyBackupStatus(action: MutableState<AsyncData<Boolean>>) = launch {
|
||||
suspend {
|
||||
encryptionService.doesBackupExistOnServer().getOrThrow()
|
||||
}.runCatchingUpdatingState(action)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.logout(
|
||||
logoutAction: MutableState<Async<String?>>,
|
||||
logoutAction: MutableState<AsyncAction<String?>>,
|
||||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package io.element.android.features.logout.impl
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
|
@ -27,7 +27,6 @@ data class LogoutState(
|
|||
val doesBackupExistOnServer: Boolean,
|
||||
val recoveryState: RecoveryState,
|
||||
val backupUploadState: BackupUploadState,
|
||||
val showConfirmationDialog: Boolean,
|
||||
val logoutAction: Async<String?>,
|
||||
val logoutAction: AsyncAction<String?>,
|
||||
val eventSink: (LogoutEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.logout.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
|
@ -30,9 +30,9 @@ open class LogoutStateProvider : PreviewParameterProvider<LogoutState> {
|
|||
aLogoutState(isLastSession = true),
|
||||
aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)),
|
||||
aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done),
|
||||
aLogoutState(showConfirmationDialog = true),
|
||||
aLogoutState(logoutAction = Async.Loading()),
|
||||
aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))),
|
||||
aLogoutState(logoutAction = AsyncAction.Confirming),
|
||||
aLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))),
|
||||
aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))),
|
||||
// Last session no recovery
|
||||
aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED),
|
||||
|
|
@ -47,15 +47,13 @@ fun aLogoutState(
|
|||
doesBackupExistOnServer: Boolean = true,
|
||||
recoveryState: RecoveryState = RecoveryState.ENABLED,
|
||||
backupUploadState: BackupUploadState = BackupUploadState.Unknown,
|
||||
showConfirmationDialog: Boolean = false,
|
||||
logoutAction: Async<String?> = Async.Uninitialized,
|
||||
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
) = LogoutState(
|
||||
isLastSession = isLastSession,
|
||||
backupState = backupState,
|
||||
doesBackupExistOnServer = doesBackupExistOnServer,
|
||||
recoveryState = recoveryState,
|
||||
backupUploadState = backupUploadState,
|
||||
showConfirmationDialog = showConfirmationDialog,
|
||||
logoutAction = logoutAction,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.impl.tools.isBackingUp
|
||||
import io.element.android.features.logout.impl.ui.LogoutActionDialog
|
||||
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -79,20 +78,11 @@ fun LogoutView(
|
|||
},
|
||||
)
|
||||
|
||||
// Log out confirmation dialog
|
||||
if (state.showConfirmationDialog) {
|
||||
LogoutConfirmationDialog(
|
||||
onSubmitClicked = {
|
||||
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
},
|
||||
onDismiss = {
|
||||
eventSink(LogoutEvents.CloseDialogs)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LogoutActionDialog(
|
||||
state.logoutAction,
|
||||
onConfirmClicked = {
|
||||
eventSink(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
},
|
||||
onForceLogoutClicked = {
|
||||
eventSink(LogoutEvents.Logout(ignoreSdkError = true))
|
||||
},
|
||||
|
|
@ -148,13 +138,13 @@ private fun ColumnScope.Buttons(
|
|||
)
|
||||
}
|
||||
val signOutSubmitRes = when {
|
||||
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
|
||||
logoutAction is AsyncAction.Loading -> R.string.screen_signout_in_progress_dialog_content
|
||||
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
|
||||
else -> CommonStrings.action_signout
|
||||
}
|
||||
Button(
|
||||
text = stringResource(id = signOutSubmitRes),
|
||||
showProgress = logoutAction is Async.Loading,
|
||||
showProgress = logoutAction is AsyncAction.Loading,
|
||||
destructive = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
|||
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.impl.tools.isBackingUp
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -54,8 +54,8 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
override fun present(): DirectLogoutState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val logoutAction: MutableState<Async<String?>> = remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
val logoutAction: MutableState<AsyncAction<String?>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
|
||||
|
|
@ -70,7 +70,6 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
}
|
||||
.collectAsState(initial = BackupUploadState.Unknown)
|
||||
|
||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||
var isLastSession by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
|
||||
|
|
@ -79,16 +78,14 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
fun handleEvents(event: DirectLogoutEvents) {
|
||||
when (event) {
|
||||
is DirectLogoutEvents.Logout -> {
|
||||
if (showLogoutDialog || event.ignoreSdkError) {
|
||||
showLogoutDialog = false
|
||||
if (logoutAction.value.isConfirming() || event.ignoreSdkError) {
|
||||
localCoroutineScope.logout(logoutAction, event.ignoreSdkError)
|
||||
} else {
|
||||
showLogoutDialog = true
|
||||
logoutAction.value = AsyncAction.Confirming
|
||||
}
|
||||
}
|
||||
DirectLogoutEvents.CloseDialogs -> {
|
||||
logoutAction.value = Async.Uninitialized
|
||||
showLogoutDialog = false
|
||||
logoutAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,14 +93,13 @@ class DefaultDirectLogoutPresenter @Inject constructor(
|
|||
return DirectLogoutState(
|
||||
canDoDirectSignOut = !isLastSession &&
|
||||
!backupUploadState.isBackingUp(),
|
||||
showConfirmationDialog = showLogoutDialog,
|
||||
logoutAction = logoutAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.logout(
|
||||
logoutAction: MutableState<Async<String?>>,
|
||||
logoutAction: MutableState<AsyncAction<String?>>,
|
||||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
|||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.logout.impl.ui.LogoutActionDialog
|
||||
import io.element.android.features.logout.impl.ui.LogoutConfirmationDialog
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -34,20 +33,11 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
|||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
) {
|
||||
val eventSink = state.eventSink
|
||||
// Log out confirmation dialog
|
||||
if (state.showConfirmationDialog) {
|
||||
LogoutConfirmationDialog(
|
||||
onSubmitClicked = {
|
||||
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
},
|
||||
onDismiss = {
|
||||
eventSink(DirectLogoutEvents.CloseDialogs)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LogoutActionDialog(
|
||||
state.logoutAction,
|
||||
onConfirmClicked = {
|
||||
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
},
|
||||
onForceLogoutClicked = {
|
||||
eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,22 +20,30 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.logout.impl.R
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LogoutActionDialog(
|
||||
state: Async<String?>,
|
||||
state: AsyncAction<String?>,
|
||||
onConfirmClicked: () -> Unit,
|
||||
onForceLogoutClicked: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
onDismissError: () -> Unit, // TODO Rename
|
||||
onSuccessLogout: (String?) -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is Async.Loading ->
|
||||
AsyncAction.Uninitialized ->
|
||||
Unit
|
||||
AsyncAction.Confirming ->
|
||||
LogoutConfirmationDialog(
|
||||
onSubmitClicked = onConfirmClicked,
|
||||
onDismiss = onDismissError
|
||||
)
|
||||
is AsyncAction.Loading ->
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
is Async.Failure ->
|
||||
is AsyncAction.Failure ->
|
||||
RetryDialog(
|
||||
title = stringResource(id = CommonStrings.dialog_title_error),
|
||||
content = stringResource(id = CommonStrings.error_unknown),
|
||||
|
|
@ -43,9 +51,7 @@ fun LogoutActionDialog(
|
|||
onRetry = onForceLogoutClicked,
|
||||
onDismiss = onDismissError,
|
||||
)
|
||||
Async.Uninitialized ->
|
||||
Unit
|
||||
is Async.Success ->
|
||||
is AsyncAction.Success ->
|
||||
LaunchedEffect(state) {
|
||||
onSuccessLogout(state.data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ package io.element.android.features.logout.impl
|
|||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -32,7 +33,6 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -50,14 +50,13 @@ class LogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.isLastSession).isFalse()
|
||||
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
||||
assertThat(initialState.doesBackupExistOnServer).isTrue()
|
||||
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +74,7 @@ class LogoutPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.isLastSession).isTrue()
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +99,7 @@ class LogoutPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.isLastSession).isFalse()
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
skipItems(1)
|
||||
val waitingState = awaitItem()
|
||||
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
|
||||
|
|
@ -120,13 +117,13 @@ class LogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
initialState.eventSink.invoke(LogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showConfirmationDialog).isFalse()
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,17 +133,15 @@ class LogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,22 +156,18 @@ class LogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
skipItems(1)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE))
|
||||
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
errorState.eventSink.invoke(LogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,28 +182,28 @@ class LogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
skipItems(1)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<LogoutState>(A_THROWABLE))
|
||||
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true))
|
||||
val loadingState2 = awaitItem()
|
||||
assertThat(loadingState2.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(2)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createLogoutPresenter(
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package io.element.android.features.logout.impl.direct
|
|||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
|
@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -49,10 +48,9 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.canDoDirectSignOut).isTrue()
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,8 +67,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +88,7 @@ class DefaultDirectLogoutPresenterTest {
|
|||
skipItems(2)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.canDoDirectSignOut).isFalse()
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,13 +98,13 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.showConfirmationDialog).isFalse()
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,17 +114,15 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,21 +137,18 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<DirectLogoutState>(A_THROWABLE))
|
||||
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs)
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,27 +163,28 @@ class DefaultDirectLogoutPresenterTest {
|
|||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
val confirmationState = awaitItem()
|
||||
assertThat(confirmationState.showConfirmationDialog).isTrue()
|
||||
assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.Confirming)
|
||||
confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false))
|
||||
skipItems(1)
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val errorState = awaitItem()
|
||||
assertThat(errorState.logoutAction).isEqualTo(Async.Failure<DirectLogoutState>(A_THROWABLE))
|
||||
assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(A_THROWABLE))
|
||||
errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true))
|
||||
val loadingState2 = awaitItem()
|
||||
assertThat(loadingState2.showConfirmationDialog).isFalse()
|
||||
assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java)
|
||||
assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java)
|
||||
assertThat(successState.logoutAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createDefaultDirectLogoutPresenter(
|
||||
matrixClient: MatrixClient = FakeMatrixClient(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.features.poll.api.create.CreatePollMode
|
||||
|
|
@ -253,6 +254,23 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
)
|
||||
overlay.show(navTarget)
|
||||
}
|
||||
is TimelineItemStickerContent -> {
|
||||
/* Sticker may have an empty url and no thumbnail
|
||||
if encrypted on certain bridges */
|
||||
if (event.content.preferredMediaSource != null) {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
name = event.content.body,
|
||||
mimeType = event.content.mimeType,
|
||||
formattedFileSize = event.content.formattedFileSize,
|
||||
fileExtension = event.content.fileExtension
|
||||
),
|
||||
mediaSource = event.content.preferredMediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
)
|
||||
overlay.show(navTarget)
|
||||
}
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
mediaInfo = MediaInfo(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -64,7 +65,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
|||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
|
|
@ -140,11 +141,11 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value)
|
||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION_SENT, updateKey = syncUpdateFlow.value)
|
||||
val roomName: Async<String> by remember {
|
||||
derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized }
|
||||
val roomName: AsyncData<String> by remember {
|
||||
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
}
|
||||
val roomAvatar: Async<AvatarData> by remember {
|
||||
derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized }
|
||||
val roomAvatar: AsyncData<AvatarData> by remember {
|
||||
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
}
|
||||
|
||||
var hasDismissedInviteDialog by rememberSaveable {
|
||||
|
|
@ -161,7 +162,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) }
|
||||
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
|
||||
var showReinvitePrompt by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) {
|
||||
withContext(dispatchers.io) {
|
||||
|
|
@ -278,8 +279,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
|
||||
inviteProgress.value = Async.Loading()
|
||||
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {
|
||||
inviteProgress.value = AsyncData.Loading()
|
||||
runCatching {
|
||||
room.updateMembers()
|
||||
|
||||
|
|
@ -295,10 +296,10 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
}.getOrThrow()
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
inviteProgress.value = Async.Success(Unit)
|
||||
inviteProgress.value = AsyncData.Success(Unit)
|
||||
},
|
||||
onFailure = {
|
||||
inviteProgress.value = Async.Failure(it)
|
||||
inviteProgress.value = AsyncData.Failure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -351,6 +352,12 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
type = AttachmentThumbnailType.Image,
|
||||
blurHash = targetEvent.content.blurhash,
|
||||
)
|
||||
is TimelineItemStickerContent -> AttachmentThumbnailInfo(
|
||||
thumbnailSource = targetEvent.content.thumbnailSource ?: targetEvent.content.mediaSource,
|
||||
textContent = targetEvent.content.body,
|
||||
type = AttachmentThumbnailType.Image,
|
||||
blurHash = targetEvent.content.blurhash,
|
||||
)
|
||||
is TimelineItemVideoContent -> AttachmentThumbnailInfo(
|
||||
thumbnailSource = targetEvent.content.thumbnailSource,
|
||||
textContent = targetEvent.content.body,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
|
|||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -33,8 +33,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
@Immutable
|
||||
data class MessagesState(
|
||||
val roomId: RoomId,
|
||||
val roomName: Async<String>,
|
||||
val roomAvatar: Async<AvatarData>,
|
||||
val roomName: AsyncData<String>,
|
||||
val roomAvatar: AsyncData<AvatarData>,
|
||||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToRedact: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
|
|
@ -48,7 +48,7 @@ data class MessagesState(
|
|||
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val inviteProgress: Async<Unit>,
|
||||
val inviteProgress: AsyncData<Unit>,
|
||||
val showReinvitePrompt: Boolean,
|
||||
val enableTextFormatting: Boolean,
|
||||
val enableVoiceMessages: Boolean,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
|
|||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
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.core.RoomId
|
||||
|
|
@ -47,8 +47,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
aMessagesState().copy(userHasPermissionToSendMessage = false),
|
||||
aMessagesState().copy(showReinvitePrompt = true),
|
||||
aMessagesState().copy(
|
||||
roomName = Async.Uninitialized,
|
||||
roomAvatar = Async.Uninitialized,
|
||||
roomName = AsyncData.Uninitialized,
|
||||
roomAvatar = AsyncData.Uninitialized,
|
||||
),
|
||||
aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)),
|
||||
aMessagesState().copy(
|
||||
|
|
@ -83,8 +83,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
|||
|
||||
fun aMessagesState() = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = Async.Success("Room name"),
|
||||
roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||
roomName = AsyncData.Success("Room name"),
|
||||
roomAvatar = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||
userHasPermissionToSendMessage = true,
|
||||
userHasPermissionToRedact = false,
|
||||
userHasPermissionToSendReaction = true,
|
||||
|
|
@ -117,7 +117,7 @@ fun aMessagesState() = MessagesState(
|
|||
),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
inviteProgress = Async.Uninitialized,
|
||||
inviteProgress = AsyncData.Uninitialized,
|
||||
showReinvitePrompt = false,
|
||||
enableTextFormatting = true,
|
||||
enableVoiceMessages = true,
|
||||
|
|
|
|||
|
|
@ -152,7 +152,11 @@ class ActionListPresenter @Inject constructor(
|
|||
add(TimelineItemAction.Reply)
|
||||
}
|
||||
}
|
||||
add(TimelineItemAction.Forward)
|
||||
// Stickers can't be forwarded (yet) so we don't show the option
|
||||
// See https://github.com/element-hq/element-x-android/issues/2161
|
||||
if (!timelineItem.isSticker) {
|
||||
add(TimelineItemAction.Forward)
|
||||
}
|
||||
}
|
||||
if (timelineItem.isMine && timelineItem.isTextMessage) {
|
||||
add(TimelineItemAction.Edit)
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -239,6 +240,9 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
is TimelineItemImageContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
is TimelineItemStickerContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
is TimelineItemVideoContent -> {
|
||||
content = { ContentForBody(event.content.body) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.compose.runtime.remember
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -48,7 +48,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
|||
fun create(eventId: String): ForwardMessagesPresenter
|
||||
}
|
||||
|
||||
private val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = mutableStateOf(Async.Uninitialized)
|
||||
private val forwardingActionState: MutableState<AsyncData<ImmutableList<RoomId>>> = mutableStateOf(AsyncData.Uninitialized)
|
||||
|
||||
fun onRoomSelected(roomIds: List<RoomId>) {
|
||||
matrixCoroutineScope.forwardEvent(eventId, roomIds.toPersistentList(), forwardingActionState)
|
||||
|
|
@ -62,13 +62,13 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
|||
|
||||
fun handleEvents(event: ForwardMessagesEvents) {
|
||||
when (event) {
|
||||
ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized
|
||||
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncData.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
return ForwardMessagesState(
|
||||
isForwarding = forwardingActionState.value.isLoading(),
|
||||
error = (forwardingActionState.value as? Async.Failure)?.error,
|
||||
error = (forwardingActionState.value as? AsyncData.Failure)?.error,
|
||||
forwardingSucceeded = forwardingSucceeded,
|
||||
eventSink = { handleEvents(it) }
|
||||
)
|
||||
|
|
@ -77,12 +77,12 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
|||
private fun CoroutineScope.forwardEvent(
|
||||
eventId: EventId,
|
||||
roomIds: ImmutableList<RoomId>,
|
||||
isForwardMessagesState: MutableState<Async<ImmutableList<RoomId>>>,
|
||||
isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
|
||||
) = launch {
|
||||
isForwardMessagesState.value = Async.Loading()
|
||||
isForwardMessagesState.value = AsyncData.Loading()
|
||||
room.forwardEvent(eventId, roomIds).fold(
|
||||
{ isForwardMessagesState.value = Async.Success(roomIds) },
|
||||
{ isForwardMessagesState.value = Async.Failure(it) }
|
||||
{ isForwardMessagesState.value = AsyncData.Success(roomIds) },
|
||||
{ isForwardMessagesState.value = AsyncData.Failure(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
|
@ -60,14 +60,14 @@ class ReportMessagePresenter @AssistedInject constructor(
|
|||
val coroutineScope = rememberCoroutineScope()
|
||||
var reason by rememberSaveable { mutableStateOf("") }
|
||||
var blockUser by rememberSaveable { mutableStateOf(false) }
|
||||
var result: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
var result: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvents(event: ReportMessageEvents) {
|
||||
when (event) {
|
||||
is ReportMessageEvents.UpdateReason -> reason = event.reason
|
||||
ReportMessageEvents.ToggleBlockUser -> blockUser = !blockUser
|
||||
ReportMessageEvents.Report -> coroutineScope.report(inputs.eventId, inputs.senderId, reason, blockUser, result)
|
||||
ReportMessageEvents.ClearError -> result.value = Async.Uninitialized
|
||||
ReportMessageEvents.ClearError -> result.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ class ReportMessagePresenter @AssistedInject constructor(
|
|||
userId: UserId,
|
||||
reason: String,
|
||||
blockUser: Boolean,
|
||||
result: MutableState<Async<Unit>>,
|
||||
result: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
result.runUpdatingState {
|
||||
val userIdToBlock = userId.takeIf { blockUser }
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
package io.element.android.features.messages.impl.report
|
||||
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class ReportMessageState(
|
||||
val reason: String,
|
||||
val blockUser: Boolean,
|
||||
val result: Async<Unit>,
|
||||
val result: AsyncAction<Unit>,
|
||||
val eventSink: (ReportMessageEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
package io.element.android.features.messages.impl.report
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageState> {
|
||||
override val values: Sequence<ReportMessageState>
|
||||
|
|
@ -25,9 +25,9 @@ open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageSt
|
|||
aReportMessageState(),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic."),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Loading()),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Failure(Throwable("error"))),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Success(Unit)),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Loading),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Failure(Throwable("error"))),
|
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = AsyncAction.Success(Unit)),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageSt
|
|||
fun aReportMessageState(
|
||||
reason: String = "",
|
||||
blockUser: Boolean = false,
|
||||
result: Async<Unit> = Async.Uninitialized,
|
||||
result: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = ReportMessageState(
|
||||
reason = reason,
|
||||
blockUser = blockUser,
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncView
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -62,10 +62,10 @@ fun ReportMessageView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val isSending = state.result is Async.Loading
|
||||
AsyncView(
|
||||
val isSending = state.result is AsyncAction.Loading
|
||||
AsyncActionView(
|
||||
async = state.result,
|
||||
showProgressDialog = false,
|
||||
progressDialog = {},
|
||||
onSuccess = { onBackClicked() },
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
onErrorDismiss = { state.eventSink(ReportMessageEvents.ClearError) }
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import androidx.compose.foundation.border
|
|||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
|
@ -41,6 +43,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider
|
||||
|
|
@ -52,7 +56,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
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.CommonDrawables
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
@ -114,8 +119,9 @@ sealed interface MessagesReactionsButtonContent {
|
|||
val isHighlighted get() = this is Reaction && reaction.isHighlighted
|
||||
}
|
||||
|
||||
private val reactionEmojiLineHeight = 20.sp
|
||||
private val addEmojiSize = 16.dp
|
||||
internal val REACTION_EMOJI_LINE_HEIGHT = 20.sp
|
||||
internal const val REACTION_IMAGE_ASPECT_RATIO = 1.0f
|
||||
private val ADD_EMOJI_SIZE = 16.dp
|
||||
|
||||
@Composable
|
||||
private fun TextContent(
|
||||
|
|
@ -123,7 +129,7 @@ private fun TextContent(
|
|||
modifier: Modifier = Modifier,
|
||||
) = Text(
|
||||
modifier = modifier
|
||||
.height(reactionEmojiLineHeight.toDp()),
|
||||
.height(REACTION_EMOJI_LINE_HEIGHT.toDp()),
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.materialColors.primary
|
||||
|
|
@ -138,7 +144,7 @@ private fun IconContent(
|
|||
contentDescription = stringResource(id = R.string.screen_room_timeline_add_reaction),
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
modifier = modifier
|
||||
.size(addEmojiSize)
|
||||
.size(ADD_EMOJI_SIZE)
|
||||
|
||||
)
|
||||
|
||||
|
|
@ -150,13 +156,25 @@ private fun ReactionContent(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = reaction.displayKey,
|
||||
style = ElementTheme.typography.fontBodyMdRegular.copy(
|
||||
fontSize = 15.sp,
|
||||
lineHeight = reactionEmojiLineHeight,
|
||||
),
|
||||
)
|
||||
// Check if this is a custom reaction (MSC4027)
|
||||
if (reaction.key.startsWith("mxc://")) {
|
||||
AsyncImage(
|
||||
modifier = modifier
|
||||
.heightIn(min = REACTION_EMOJI_LINE_HEIGHT.toDp(), max = REACTION_EMOJI_LINE_HEIGHT.toDp())
|
||||
.aspectRatio(REACTION_IMAGE_ASPECT_RATIO, false),
|
||||
model = MediaRequestData(MediaSource(reaction.key), MediaRequestData.Kind.Content),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
else {
|
||||
Text(
|
||||
text = reaction.displayKey,
|
||||
style = ElementTheme.typography.fontBodyMdRegular.copy(
|
||||
fontSize = 15.sp,
|
||||
lineHeight = REACTION_EMOJI_LINE_HEIGHT,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (reaction.count > 1) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -69,7 +70,8 @@ fun TimelineEventTimestampView(
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.then(clickModifier)
|
||||
.padding(start = 16.dp) // Add extra padding for touch target size
|
||||
// Add extra padding for touch target size
|
||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
|
@ -107,3 +109,7 @@ internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEve
|
|||
onLongClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
object TimelineEventTimestampViewDefaults {
|
||||
val spacing = 16.dp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
|
|
@ -67,7 +68,8 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents
|
|||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||
|
|
@ -78,7 +80,9 @@ import io.element.android.features.messages.impl.timeline.model.bubble.BubbleSta
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||
|
|
@ -447,12 +451,13 @@ private fun MessageEventBubbleContent(
|
|||
fun WithTimestampLayout(
|
||||
timestampPosition: TimestampPosition,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
canShrinkContent: Boolean = false,
|
||||
content: @Composable (onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
||||
) {
|
||||
when (timestampPosition) {
|
||||
TimestampPosition.Overlay ->
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
content()
|
||||
content {}
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
|
|
@ -465,20 +470,26 @@ private fun MessageEventBubbleContent(
|
|||
)
|
||||
}
|
||||
TimestampPosition.Aligned ->
|
||||
Box(modifier) {
|
||||
content()
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
ContentAvoidingLayout(
|
||||
modifier = modifier,
|
||||
// The spacing is negative to make the content overlap the empty space at the start of the timestamp
|
||||
spacing = (-4).dp,
|
||||
overlayOffset = DpOffset(0.dp, -1.dp),
|
||||
shrinkContent = canShrinkContent,
|
||||
content = { content(this::onContentLayoutChanged) },
|
||||
overlay = {
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
TimestampPosition.Below ->
|
||||
Column(modifier) {
|
||||
content()
|
||||
content {}
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
|
|
@ -497,7 +508,8 @@ private fun MessageEventBubbleContent(
|
|||
timestampPosition: TimestampPosition,
|
||||
showThreadDecoration: Boolean,
|
||||
inReplyToDetails: InReplyToDetails?,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
canShrinkContent: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val timestampLayoutModifier: Modifier
|
||||
|
|
@ -514,7 +526,8 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
timestampPosition != TimestampPosition.Overlay -> {
|
||||
timestampLayoutModifier = Modifier
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
contentModifier = Modifier
|
||||
.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
}
|
||||
else -> {
|
||||
timestampLayoutModifier = Modifier
|
||||
|
|
@ -529,8 +542,9 @@ private fun MessageEventBubbleContent(
|
|||
val contentWithTimestamp = @Composable {
|
||||
WithTimestampLayout(
|
||||
timestampPosition = timestampPosition,
|
||||
canShrinkContent = canShrinkContent,
|
||||
modifier = timestampLayoutModifier,
|
||||
) {
|
||||
) { onContentLayoutChanged ->
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
onLinkClicked = { url ->
|
||||
|
|
@ -547,9 +561,9 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
}
|
||||
},
|
||||
extraPadding = event.toExtraPadding(),
|
||||
eventSink = eventSink,
|
||||
modifier = contentModifier,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = contentModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -583,6 +597,7 @@ private fun MessageEventBubbleContent(
|
|||
|
||||
val timestampPosition = when (event.content) {
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemStickerContent,
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemLocationContent -> TimestampPosition.Overlay
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
|
|
@ -592,6 +607,7 @@ private fun MessageEventBubbleContent(
|
|||
showThreadDecoration = event.isThreaded,
|
||||
timestampPosition = timestampPosition,
|
||||
inReplyToDetails = event.inReplyTo,
|
||||
canShrinkContent = event.content is TimelineItemVoiceContent,
|
||||
modifier = bubbleModifier
|
||||
)
|
||||
}
|
||||
|
|
@ -653,7 +669,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
|||
isMine = it,
|
||||
content = aTimelineItemTextContent().copy(
|
||||
body = "A long text which will be displayed on several lines and" +
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.First,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
|
|
@ -38,18 +37,15 @@ internal fun TimelineItemEventRowTimestampPreview(
|
|||
"Text longer, displayed on 1 line",
|
||||
"Text which should be rendered on several lines",
|
||||
).forEach { str ->
|
||||
listOf(false, true).forEach { useDocument ->
|
||||
ATimelineItemEventRow(
|
||||
event = event.copy(
|
||||
content = oldContent.copy(
|
||||
body = str,
|
||||
htmlDocument = if (useDocument) Jsoup.parse(str) else null,
|
||||
),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
senderDisplayName = if (useDocument) "Document case" else "Text case",
|
||||
ATimelineItemEventRow(
|
||||
event = event.copy(
|
||||
content = oldContent.copy(
|
||||
body = str,
|
||||
),
|
||||
)
|
||||
}
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
senderDisplayName = "A sender",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageConten
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
|
|
@ -109,6 +110,10 @@ class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> {
|
|||
body = "Image",
|
||||
type = ImageMessageType("Image", MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "Sticker",
|
||||
type = StickerMessageType("Image", MediaSource("url"), null),
|
||||
),
|
||||
aMessageContent(
|
||||
body = "File",
|
||||
type = FileMessageType("File", MediaSource("url"), null),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import androidx.compose.ui.zIndex
|
|||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||
|
|
@ -81,7 +80,6 @@ fun TimelineItemStateEventRow(
|
|||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
onLinkClicked = {},
|
||||
extraPadding = noExtraPadding,
|
||||
eventSink = eventSink,
|
||||
modifier = Modifier.defaultTimelineContentPadding()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -44,15 +46,17 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
@Composable
|
||||
fun TimelineItemAudioView(
|
||||
content: TimelineItemAudioContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val iconSize = 32.dp
|
||||
val spacing = 8.dp
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.size(iconSize)
|
||||
.clip(CircleShape)
|
||||
.background(ElementTheme.materialColors.background),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -65,7 +69,7 @@ fun TimelineItemAudioView(
|
|||
.size(16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.width(spacing))
|
||||
Column {
|
||||
Text(
|
||||
text = content.body,
|
||||
|
|
@ -75,11 +79,15 @@ fun TimelineItemAudioView(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = content.fileExtensionAndSize + extraPadding.getStr(ElementTheme.typography.fontBodySmRegular),
|
||||
text = content.fileExtensionAndSize,
|
||||
color = ElementTheme.materialColors.secondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = ContentAvoidingLayout.measureLastTextLine(
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
extraWidth = iconSize + spacing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +99,6 @@ internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioCon
|
|||
ElementPreview {
|
||||
TimelineItemAudioView(
|
||||
content,
|
||||
extraPadding = noExtraPadding,
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -29,14 +30,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun TimelineItemEncryptedView(
|
||||
@Suppress("UNUSED_PARAMETER") content: TimelineItemEncryptedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
|
||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||
iconResourceId = CommonDrawables.ic_waiting_to_decrypt,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -48,6 +49,6 @@ internal fun TimelineItemEncryptedViewPreview() = ElementPreview {
|
|||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.Unknown
|
||||
),
|
||||
extraPadding = noExtraPadding
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||
import io.element.android.features.messages.impl.timeline.di.rememberPresenter
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||
|
|
@ -30,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
|
|
@ -40,32 +42,32 @@ import io.element.android.libraries.architecture.Presenter
|
|||
@Composable
|
||||
fun TimelineItemEventContentView(
|
||||
content: TimelineItemEventContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onLinkClicked: (url: String) -> Unit,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit = {},
|
||||
) {
|
||||
val presenterFactories = LocalTimelineItemPresenterFactories.current
|
||||
when (content) {
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onContentLayoutChanged = onContentLayoutChanged
|
||||
)
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemLocationContent -> TimelineItemLocationView(
|
||||
|
|
@ -76,18 +78,22 @@ fun TimelineItemEventContentView(
|
|||
content = content,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemStickerContent -> TimelineItemStickerView(
|
||||
content = content,
|
||||
modifier = modifier,
|
||||
)
|
||||
is TimelineItemVideoContent -> TimelineItemVideoView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemFileContent -> TimelineItemFileView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemAudioContent -> TimelineItemAudioView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemStateContent -> TimelineItemStateView(
|
||||
|
|
@ -104,7 +110,7 @@ fun TimelineItemEventContentView(
|
|||
TimelineItemVoiceView(
|
||||
state = presenter.present(),
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -44,15 +46,17 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables
|
|||
@Composable
|
||||
fun TimelineItemFileView(
|
||||
content: TimelineItemFileContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val iconSize = 32.dp
|
||||
val spacing = 8.dp
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.size(iconSize)
|
||||
.clip(CircleShape)
|
||||
.background(ElementTheme.materialColors.background),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -66,7 +70,7 @@ fun TimelineItemFileView(
|
|||
.rotate(-45f),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(Modifier.width(spacing))
|
||||
Column {
|
||||
Text(
|
||||
text = content.body,
|
||||
|
|
@ -76,11 +80,15 @@ fun TimelineItemFileView(
|
|||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = content.fileExtensionAndSize + extraPadding.getStr(textStyle = ElementTheme.typography.fontBodySmRegular),
|
||||
text = content.fileExtensionAndSize,
|
||||
color = ElementTheme.materialColors.secondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = ContentAvoidingLayout.measureLastTextLine(
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
extraWidth = iconSize + spacing
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +99,6 @@ fun TimelineItemFileView(
|
|||
internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) = ElementPreview {
|
||||
TimelineItemFileView(
|
||||
content,
|
||||
extraPadding = noExtraPadding,
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -39,12 +41,19 @@ fun TimelineItemInformativeView(
|
|||
text: String,
|
||||
iconDescription: String,
|
||||
@DrawableRes iconResourceId: Int,
|
||||
extraPadding: ExtraPadding,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = modifier.onSizeChanged { size ->
|
||||
onContentLayoutChanged(
|
||||
ContentAvoidingLayoutData(
|
||||
contentWidth = size.width,
|
||||
contentHeight = size.height,
|
||||
)
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
resourceId = iconResourceId,
|
||||
|
|
@ -57,7 +66,7 @@ fun TimelineItemInformativeView(
|
|||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = text + extraPadding.getStr(textStyle = ElementTheme.typography.fontBodyMdRegular)
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +78,6 @@ internal fun TimelineItemInformativeViewPreview() = ElementPreview {
|
|||
text = "Info",
|
||||
iconDescription = "",
|
||||
iconResourceId = CompoundDrawables.ic_delete,
|
||||
extraPadding = noExtraPadding,
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -28,14 +29,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
@Composable
|
||||
fun TimelineItemRedactedView(
|
||||
@Suppress("UNUSED_PARAMETER") content: TimelineItemRedactedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = CommonStrings.common_message_removed),
|
||||
iconDescription = stringResource(id = CommonStrings.common_message_removed),
|
||||
iconResourceId = CompoundDrawables.ic_delete,
|
||||
extraPadding = extraPadding,
|
||||
onContentLayoutChanged = onContentLayoutChanged,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -45,6 +46,6 @@ fun TimelineItemRedactedView(
|
|||
internal fun TimelineItemRedactedViewPreview() = ElementPreview {
|
||||
TimelineItemRedactedView(
|
||||
TimelineItemRedactedContent,
|
||||
extraPadding = noExtraPadding
|
||||
onContentLayoutChanged = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
|
||||
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
||||
private const val STICKER_SIZE_IN_DP = 128
|
||||
private const val DEFAULT_ASPECT_RATIO = 1.33f
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStickerView(
|
||||
content: TimelineItemStickerContent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val safeAspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO
|
||||
Box(
|
||||
modifier = modifier
|
||||
.heightIn(min = STICKER_SIZE_IN_DP.dp, max = STICKER_SIZE_IN_DP.dp)
|
||||
.aspectRatio(safeAspectRatio, false),
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
BlurHashAsyncImage(
|
||||
model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)),
|
||||
blurHash = content.blurhash,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemStickerContentProvider::class) content: TimelineItemStickerContent) = ElementPreview {
|
||||
TimelineItemStickerView(content)
|
||||
}
|
||||
|
|
@ -22,12 +22,11 @@ import androidx.compose.material3.LocalContentColor
|
|||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.buildSpannedString
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -38,9 +37,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText
|
|||
@Composable
|
||||
fun TimelineItemTextView(
|
||||
content: TimelineItemTextBasedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
onLinkClicked: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onContentLayoutChanged: (ContentAvoidingLayoutData) -> Unit = {},
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||
|
|
@ -49,19 +48,14 @@ fun TimelineItemTextView(
|
|||
|
||||
val formattedBody = content.formattedBody
|
||||
val body = SpannableString(formattedBody ?: content.body)
|
||||
val extraPaddingText = extraPadding.getStr()
|
||||
|
||||
Box(modifier) {
|
||||
val textWithPadding = remember(body) {
|
||||
buildSpannedString {
|
||||
append(body)
|
||||
append(extraPaddingText)
|
||||
}
|
||||
}
|
||||
EditorStyledText(
|
||||
text = textWithPadding,
|
||||
text = body,
|
||||
onLinkClickedListener = onLinkClicked,
|
||||
style = ElementRichTextEditorStyle.textStyle(),
|
||||
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChanged = onContentLayoutChanged),
|
||||
releaseOnDetach = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +68,6 @@ internal fun TimelineItemTextViewPreview(
|
|||
) = ElementPreview {
|
||||
TimelineItemTextView(
|
||||
content = content,
|
||||
extraPadding = ExtraPadding(extraWidth = 32.dp),
|
||||
onLinkClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue