Merge branch 'develop' into renovate/compose.bom

This commit is contained in:
ganfra 2024-11-15 11:31:39 +01:00 committed by GitHub
commit bda197ef20
142 changed files with 583 additions and 850 deletions

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
variant: [debug, release, nightly, samples] variant: [debug, release, nightly]
fail-fast: false fail-fast: false
# Allow all jobs on develop. Just one per PR. # Allow all jobs on develop. Just one per PR.
concurrency: concurrency:
@ -82,6 +82,3 @@ jobs:
- name: Compile nightly sources - name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }} if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile samples minimal
if: ${{ matrix.variant == 'samples' }}
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES

View file

@ -49,8 +49,6 @@ Please ensure that you're using the project formatting rules (which are in the p
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`. This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
Note: please make sure that the configuration is `app` and not `samples.minimal`.
## Strings ## Strings
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS. The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS.

View file

@ -40,7 +40,7 @@ We want:
The CI checks that: The CI checks that:
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app 1. The code is compiling, without any warnings, for all the app build types and variants
2. The tests are passing 2. The tests are passing
3. The code quality is good (detekt, ktlint, lint) 3. The code quality is good (detekt, ktlint, lint)
4. The code is running and smoke tests are passing (maestro) 4. The code is running and smoke tests are passing (maestro)

View file

@ -19,6 +19,4 @@ data class CreateRoomConfig(
val avatarUri: Uri? = null, val avatarUri: Uri? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(), val invites: ImmutableList<MatrixUser> = persistentListOf(),
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private, val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
) { )
val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid()
}

View file

@ -11,13 +11,13 @@ import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomAccess import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomAddressErrorState
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -29,6 +29,7 @@ import javax.inject.Inject
@SingleIn(CreateRoomScope::class) @SingleIn(CreateRoomScope::class)
class CreateRoomDataStore @Inject constructor( class CreateRoomDataStore @Inject constructor(
val selectedUserListDataStore: UserListDataStore, val selectedUserListDataStore: UserListDataStore,
private val roomAliasHelper: RoomAliasHelper,
) { ) {
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig()) private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null private var cachedAvatarUri: Uri? = null
@ -46,13 +47,13 @@ class CreateRoomDataStore @Inject constructor(
fun setRoomName(roomName: String) { fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config -> createRoomConfigFlow.getAndUpdate { config ->
/*
val newVisibility = when (config.roomVisibility) { val newVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> { is RoomVisibilityState.Public -> {
val roomAddress = config.roomVisibility.roomAddress val roomAddress = config.roomVisibility.roomAddress
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) { if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
config.roomVisibility.copy( config.roomVisibility.copy(
roomAddress = RoomAddress.AutoFilled(roomName), roomAddress = RoomAddress.AutoFilled(roomAliasName),
) )
} else { } else {
config.roomVisibility config.roomVisibility
@ -60,9 +61,9 @@ class CreateRoomDataStore @Inject constructor(
} }
else -> config.roomVisibility else -> config.roomVisibility
} }
*/
config.copy( config.copy(
roomName = roomName.takeIf { it.isNotEmpty() }, roomName = roomName.takeIf { it.isNotEmpty() },
roomVisibility = newVisibility,
) )
} }
} }
@ -85,11 +86,13 @@ class CreateRoomDataStore @Inject constructor(
config.copy( config.copy(
roomVisibility = when (visibility) { roomVisibility = when (visibility) {
RoomVisibilityItem.Private -> RoomVisibilityState.Private RoomVisibilityItem.Private -> RoomVisibilityState.Private
RoomVisibilityItem.Public -> RoomVisibilityState.Public( RoomVisibilityItem.Public -> {
roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()), val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
roomAddressErrorState = RoomAddressErrorState.None, RoomVisibilityState.Public(
roomAccess = RoomAccess.Anyone, roomAddress = RoomAddress.AutoFilled(roomAliasName),
) roomAccess = RoomAccess.Anyone,
)
}
} }
) )
} }

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
@ -31,6 +32,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.roomAliasFromName
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
@ -39,9 +42,12 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class ConfigureRoomPresenter @Inject constructor( class ConfigureRoomPresenter @Inject constructor(
private val dataStore: CreateRoomDataStore, private val dataStore: CreateRoomDataStore,
@ -51,6 +57,7 @@ class ConfigureRoomPresenter @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
permissionsPresenterFactory: PermissionsPresenter.Factory, permissionsPresenterFactory: PermissionsPresenter.Factory,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val roomAliasHelper: RoomAliasHelper,
) : Presenter<ConfigureRoomState> { ) : Presenter<ConfigureRoomState> {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false private var pendingPermissionRequest = false
@ -58,9 +65,12 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable @Composable
override fun present(): ConfigureRoomState { override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present() val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig = dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig()) val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false) val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
val roomAddressValidity = remember {
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) }, onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
@ -69,12 +79,12 @@ class ConfigureRoomPresenter @Inject constructor(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) } onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) }
) )
val avatarActions by remember(createRoomConfig.value.avatarUri) { val avatarActions by remember(createRoomConfig.avatarUri) {
derivedStateOf { derivedStateOf {
listOfNotNull( listOfNotNull(
AvatarAction.TakePhoto, AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null }, AvatarAction.Remove.takeIf { createRoomConfig.avatarUri != null },
).toImmutableList() ).toImmutableList()
} }
} }
@ -86,6 +96,10 @@ class ConfigureRoomPresenter @Inject constructor(
} }
} }
RoomAddressValidityEffect(createRoomConfig.roomVisibility.roomAddress()) { newRoomAddressValidity ->
roomAddressValidity.value = newRoomAddressValidity
}
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
@ -102,7 +116,7 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess) is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress) is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value) is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvents.HandleAvatarAction -> { is ConfigureRoomEvents.HandleAvatarAction -> {
when (event.action) { when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
@ -122,15 +136,49 @@ class ConfigureRoomPresenter @Inject constructor(
return ConfigureRoomState( return ConfigureRoomState(
isKnockFeatureEnabled = isKnockFeatureEnabled, isKnockFeatureEnabled = isKnockFeatureEnabled,
config = createRoomConfig.value, config = createRoomConfig,
avatarActions = avatarActions, avatarActions = avatarActions,
createRoomAction = createRoomAction.value, createRoomAction = createRoomAction.value,
cameraPermissionState = cameraPermissionState, cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName, homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity.value,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }
@Composable
private fun RoomAddressValidityEffect(
roomAddress: Optional<String>,
onRoomAddressValidityChange: (RoomAddressValidity) -> Unit,
) {
val onChange by rememberUpdatedState(onRoomAddressValidityChange)
LaunchedEffect(roomAddress) {
val roomAliasName = roomAddress.getOrNull().orEmpty()
if (roomAliasName.isEmpty()) {
onChange(RoomAddressValidity.Unknown)
return@LaunchedEffect
}
// debounce the room address validation
delay(300)
val roomAlias = matrixClient.roomAliasFromName(roomAliasName).getOrNull()
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressValidity.InvalidSymbols)
} else {
matrixClient.resolveRoomAlias(roomAlias)
.onSuccess { resolved ->
if (resolved.isPresent) {
onChange(RoomAddressValidity.NotAvailable)
} else {
onChange(RoomAddressValidity.Valid)
}
}
.onFailure {
onChange(RoomAddressValidity.Valid)
}
}
}
}
private fun CoroutineScope.createRoom( private fun CoroutineScope.createRoom(
config: CreateRoomConfig, config: CreateRoomConfig,
createRoomAction: MutableState<AsyncAction<RoomId>> createRoomAction: MutableState<AsyncAction<RoomId>>
@ -148,7 +196,7 @@ class ConfigureRoomPresenter @Inject constructor(
preset = RoomPreset.PUBLIC_CHAT, preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId }, invite = config.invites.map { it.userId },
avatar = avatarUrl, avatar = avatarUrl,
canonicalAlias = config.roomVisibility.roomAddress() roomAliasName = config.roomVisibility.roomAddress()
) )
} else { } else {
CreateRoomParameters( CreateRoomParameters(

View file

@ -20,6 +20,10 @@ data class ConfigureRoomState(
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: AsyncAction<RoomId>, val createRoomAction: AsyncAction<RoomId>,
val cameraPermissionState: PermissionsState, val cameraPermissionState: PermissionsState,
val roomAddressValidity: RoomAddressValidity,
val homeserverName: String, val homeserverName: String,
val eventSink: (ConfigureRoomEvents) -> Unit val eventSink: (ConfigureRoomEvents) -> Unit
) ) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
}

View file

@ -28,9 +28,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(), invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public( roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"), roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking, roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
), ),
), ),
), ),
@ -40,12 +39,44 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(), invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public( roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"), roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking, roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
), ),
), ),
), ),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.NotAvailable,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.Valid,
),
) )
} }
@ -56,6 +87,7 @@ fun aConfigureRoomState(
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
homeserverName: String = "matrix.org", homeserverName: String = "matrix.org",
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
eventSink: (ConfigureRoomEvents) -> Unit = { }, eventSink: (ConfigureRoomEvents) -> Unit = { },
) = ConfigureRoomState( ) = ConfigureRoomState(
config = config, config = config,
@ -64,5 +96,6 @@ fun aConfigureRoomState(
createRoomAction = createRoomAction, createRoomAction = createRoomAction,
cameraPermissionState = cameraPermissionState, cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName, homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
@ -79,7 +80,7 @@ fun ConfigureRoomView(
modifier = modifier.clearFocusOnTap(focusManager), modifier = modifier.clearFocusOnTap(focusManager),
topBar = { topBar = {
ConfigureRoomToolbar( ConfigureRoomToolbar(
isNextActionEnabled = state.config.isValid, isNextActionEnabled = state.isValid,
onBackClick = onBackClick, onBackClick = onBackClick,
onNextClick = { onNextClick = {
focusManager.clearFocus() focusManager.clearFocus()
@ -143,8 +144,10 @@ fun ConfigureRoomView(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress, address = state.config.roomVisibility.roomAddress,
homeserverName = state.homeserverName, homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) }, onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
) )
Spacer(Modifier)
} }
} }
} }
@ -319,6 +322,7 @@ private fun RoomAccessOptions(
private fun RoomAddressField( private fun RoomAddressField(
address: RoomAddress, address: RoomAddress,
homeserverName: String, homeserverName: String,
addressValidity: RoomAddressValidity,
onAddressChange: (String) -> Unit, onAddressChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -340,7 +344,16 @@ private fun RoomAddressField(
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
) )
}, },
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer), supportingText = when (addressValidity) {
RoomAddressValidity.InvalidSymbols -> {
stringResource(R.string.screen_create_room_room_address_invalid_symbols_error_description)
}
RoomAddressValidity.NotAvailable -> {
stringResource(R.string.screen_create_room_room_address_not_available_error_description)
}
else -> stringResource(R.string.screen_create_room_room_address_section_footer)
},
isError = addressValidity.isError(),
onValueChange = onAddressChange, onValueChange = onAddressChange,
singleLine = true, singleLine = true,
) )

View file

@ -1,17 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
/**
* Represents the error state of a room address.
*/
sealed interface RoomAddressErrorState {
data object InvalidCharacters : RoomAddressErrorState
data object AlreadyExists : RoomAddressErrorState
data object None : RoomAddressErrorState
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.runtime.Immutable
/**
* Represents the validity state of a room address.
* ie. whether it contains invalid characters, is already taken, or is valid.
*/
@Immutable
sealed interface RoomAddressValidity {
data object Unknown : RoomAddressValidity
data object InvalidSymbols : RoomAddressValidity
data object NotAvailable : RoomAddressValidity
data object Valid : RoomAddressValidity
fun isError(): Boolean {
return this is InvalidSymbols || this is NotAvailable
}
}

View file

@ -14,7 +14,6 @@ sealed interface RoomVisibilityState {
data class Public( data class Public(
val roomAddress: RoomAddress, val roomAddress: RoomAddress,
val roomAddressErrorState: RoomAddressErrorState,
val roomAccess: RoomAccess, val roomAccess: RoomAccess,
) : RoomVisibilityState ) : RoomVisibilityState
@ -24,11 +23,4 @@ sealed interface RoomVisibilityState {
is Public -> Optional.of(roomAddress.value) is Public -> Optional.of(roomAddress.value)
} }
} }
fun isValid(): Boolean {
return when (this) {
is Private -> true
is Public -> roomAddressErrorState is RoomAddressErrorState.None && roomAddress.value.isNotEmpty()
}
}
} }

View file

@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -32,7 +33,7 @@ class AddPeoplePresenterTest {
presenter = AddPeoplePresenter( presenter = AddPeoplePresenter(
FakeUserListPresenterFactory(), FakeUserListPresenterFactory(),
FakeUserRepository(), FakeUserRepository(),
CreateRoomDataStore(UserListDataStore()) CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
) )
} }

View file

@ -19,11 +19,15 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
@ -44,6 +48,8 @@ import io.mockk.mockkStatic
import io.mockk.unmockkAll import io.mockk.unmockkAll
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -52,6 +58,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import java.io.File import java.io.File
import java.util.Optional
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2" private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
@ -95,21 +102,21 @@ class ConfigureRoomPresenterTest {
presenter.test { presenter.test {
val initialState = initialState() val initialState = initialState()
var config = initialState.config var config = initialState.config
assertThat(initialState.config.isValid).isFalse() assertThat(initialState.isValid).isFalse()
// Room name not empty // Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem() var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME) config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config) assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isTrue() assertThat(newState.isValid).isTrue()
// Clear room name // Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState = awaitItem() newState = awaitItem()
config = config.copy(roomName = null) config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config) assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isFalse() assertThat(newState.isValid).isFalse()
} }
} }
@ -118,8 +125,9 @@ class ConfigureRoomPresenterTest {
val userListDataStore = UserListDataStore() val userListDataStore = UserListDataStore()
val pickerProvider = FakePickerProvider() val pickerProvider = FakePickerProvider()
val permissionsPresenter = FakePermissionsPresenter() val permissionsPresenter = FakePermissionsPresenter()
val roomAliasHelper = FakeRoomAliasHelper()
val presenter = createConfigureRoomPresenter( val presenter = createConfigureRoomPresenter(
createRoomDataStore = CreateRoomDataStore(userListDataStore), createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
pickerProvider = pickerProvider, pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter, permissionsPresenter = permissionsPresenter,
) )
@ -191,8 +199,7 @@ class ConfigureRoomPresenterTest {
newState = awaitItem() newState = awaitItem()
expectedConfig = expectedConfig.copy( expectedConfig = expectedConfig.copy(
roomVisibility = RoomVisibilityState.Public( roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""), roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
roomAddressErrorState = RoomAddressErrorState.None,
roomAccess = RoomAccess.Anyone, roomAccess = RoomAccess.Anyone,
) )
) )
@ -254,7 +261,7 @@ class ConfigureRoomPresenterTest {
val matrixClient = createMatrixClient() val matrixClient = createMatrixClient()
val analyticsService = FakeAnalyticsService() val analyticsService = FakeAnalyticsService()
val mediaPreProcessor = FakeMediaPreProcessor() val mediaPreProcessor = FakeMediaPreProcessor()
val createRoomDataStore = CreateRoomDataStore(UserListDataStore()) val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
val presenter = createConfigureRoomPresenter( val presenter = createConfigureRoomPresenter(
createRoomDataStore = createRoomDataStore, createRoomDataStore = createRoomDataStore,
mediaPreProcessor = mediaPreProcessor, mediaPreProcessor = mediaPreProcessor,
@ -315,17 +322,88 @@ class ConfigureRoomPresenterTest {
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is invalid when format is invalid`() = runTest {
val aliasHelper = FakeRoomAliasHelper(
isRoomAliasValidLambda = { false }
)
val presenter = createConfigureRoomPresenter(
roomAliasHelper = aliasHelper
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is not available when alias is not available`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = false)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is valid when alias is available and format is valid`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = true)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
}
}
}
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState { private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
skipItems(1) skipItems(1)
return awaitItem() return awaitItem()
} }
private fun createMatrixClient() = FakeMatrixClient( private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
userIdServerNameLambda = { "matrix.org" }, userIdServerNameLambda = { "matrix.org" },
resolveRoomAliasResult = {
val resolvedRoomAlias = if (isAliasAvailable) {
Optional.empty()
} else {
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
}
Result.success(resolvedRoomAlias)
}
) )
private fun createConfigureRoomPresenter( private fun createConfigureRoomPresenter(
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()), roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(), matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(), pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(), mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
@ -339,6 +417,7 @@ class ConfigureRoomPresenterTest {
mediaPreProcessor = mediaPreProcessor, mediaPreProcessor = mediaPreProcessor,
analyticsService = analyticsService, analyticsService = analyticsService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
roomAliasHelper = roomAliasHelper,
featureFlagService = FakeFeatureFlagService( featureFlagService = FakeFeatureFlagService(
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled) mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
) )

View file

@ -212,7 +212,7 @@ class MessagesNode @AssistedInject constructor(
state = state, state = state,
onBackClick = this::navigateUp, onBackClick = this::navigateUp,
onRoomDetailsClick = this::onRoomDetailsClick, onRoomDetailsClick = this::onRoomDetailsClick,
onEventClick = this::onEventClick, onEventContentClick = this::onEventClick,
onPreviewAttachments = this::onPreviewAttachments, onPreviewAttachments = this::onPreviewAttachments,
onUserDataClick = this::onUserDataClick, onUserDataClick = this::onUserDataClick,
onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) }, onLinkClick = { url -> onLinkClick(activity, isDark, url, state.timelineState.eventSink) },

View file

@ -114,7 +114,7 @@ fun MessagesView(
state: MessagesState, state: MessagesState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onRoomDetailsClick: () -> Unit, onRoomDetailsClick: () -> Unit,
onEventClick: (event: TimelineItem.Event) -> Boolean, onEventContentClick: (event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit, onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
@ -142,9 +142,9 @@ fun MessagesView(
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose // This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
val localView = LocalView.current val localView = LocalView.current
fun onMessageClick(event: TimelineItem.Event) { fun onContentClick(event: TimelineItem.Event) {
Timber.v("onMessageClick= ${event.id}") Timber.v("onMessageClick= ${event.id}")
val hideKeyboard = onEventClick(event) val hideKeyboard = onEventContentClick(event)
if (hideKeyboard) { if (hideKeyboard) {
localView.hideKeyboard() localView.hideKeyboard()
} }
@ -206,7 +206,7 @@ fun MessagesView(
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
onMessageClick = ::onMessageClick, onContentClick = ::onContentClick,
onMessageLongClick = ::onMessageLongClick, onMessageLongClick = ::onMessageLongClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
@ -306,7 +306,7 @@ private fun AttachmentStateView(
@Composable @Composable
private fun MessagesViewContent( private fun MessagesViewContent(
state: MessagesState, state: MessagesState,
onMessageClick: (TimelineItem.Event) -> Unit, onContentClick: (TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit,
@ -382,7 +382,7 @@ private fun MessagesViewContent(
timelineProtectionState = state.timelineProtectionState, timelineProtectionState = state.timelineProtectionState,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onMessageClick = onMessageClick, onContentClick = onContentClick,
onMessageLongClick = onMessageLongClick, onMessageLongClick = onMessageLongClick,
onSwipeToReply = onSwipeToReply, onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,
@ -568,7 +568,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
state = state, state = state,
onBackClick = {}, onBackClick = {},
onRoomDetailsClick = {}, onRoomDetailsClick = {},
onEventClick = { false }, onEventContentClick = { false },
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onPreviewAttachments = {}, onPreviewAttachments = {},

View file

@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview(
), ),
onBackClick = {}, onBackClick = {},
onRoomDetailsClick = {}, onRoomDetailsClick = {},
onEventClick = { false }, onEventContentClick = { false },
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onPreviewAttachments = {}, onPreviewAttachments = {},

View file

@ -216,7 +216,7 @@ private fun PinnedMessagesListLoaded(
focusedEventId = null, focusedEventId = null,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onClick = onEventClick, onContentClick = onEventClick,
onLongClick = ::onMessageLongClick, onLongClick = ::onMessageLongClick,
inReplyToClick = {}, inReplyToClick = {},
onReactionClick = { _, _ -> }, onReactionClick = { _, _ -> },
@ -230,6 +230,7 @@ private fun PinnedMessagesListLoaded(
TimelineItemEventContentViewWrapper( TimelineItemEventContentViewWrapper(
event = event, event = event,
timelineProtectionState = state.timelineProtectionState, timelineProtectionState = state.timelineProtectionState,
onContentClick = { onEventClick(event) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
modifier = contentModifier, modifier = contentModifier,
onContentLayoutChange = onContentLayoutChange onContentLayoutChange = onContentLayoutChange
@ -244,6 +245,7 @@ private fun PinnedMessagesListLoaded(
private fun TimelineItemEventContentViewWrapper( private fun TimelineItemEventContentViewWrapper(
event: TimelineItem.Event, event: TimelineItem.Event,
timelineProtectionState: TimelineProtectionState, timelineProtectionState: TimelineProtectionState,
onContentClick: () -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -258,10 +260,11 @@ private fun TimelineItemEventContentViewWrapper(
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
eventSink = { }, eventSink = { },
modifier = modifier, modifier = modifier,
onContentClick = onContentClick,
onContentLayoutChange = onContentLayoutChange onContentLayoutChange = onContentLayoutChange
) )
} }

View file

@ -76,7 +76,7 @@ fun TimelineView(
timelineProtectionState: TimelineProtectionState, timelineProtectionState: TimelineProtectionState,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onMessageClick: (TimelineItem.Event) -> Unit, onContentClick: (TimelineItem.Event) -> Unit,
onMessageLongClick: (TimelineItem.Event) -> Unit, onMessageLongClick: (TimelineItem.Event) -> Unit,
onSwipeToReply: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit,
onReactionClick: (emoji: String, TimelineItem.Event) -> Unit, onReactionClick: (emoji: String, TimelineItem.Event) -> Unit,
@ -141,7 +141,7 @@ fun TimelineView(
focusedEventId = state.focusedEventId, focusedEventId = state.focusedEventId,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onClick = onMessageClick, onContentClick = onContentClick,
onLongClick = onMessageLongClick, onLongClick = onMessageLongClick,
inReplyToClick = ::inReplyToClick, inReplyToClick = ::inReplyToClick,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,
@ -322,7 +322,7 @@ internal fun TimelineViewPreview(
timelineProtectionState = aTimelineProtectionState(), timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onMessageClick = {}, onContentClick = {},
onMessageLongClick = {}, onMessageLongClick = {},
onSwipeToReply = {}, onSwipeToReply = {},
onReactionClick = { _, _ -> }, onReactionClick = { _, _ -> },

View file

@ -41,7 +41,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
timelineProtectionState = aTimelineProtectionState(), timelineProtectionState = aTimelineProtectionState(),
onUserDataClick = {}, onUserDataClick = {},
onLinkClick = {}, onLinkClick = {},
onMessageClick = {}, onContentClick = {},
onMessageLongClick = {}, onMessageLongClick = {},
onSwipeToReply = {}, onSwipeToReply = {},
onReactionClick = { _, _ -> }, onReactionClick = { _, _ -> },

View file

@ -30,7 +30,7 @@ internal fun ATimelineItemEventRow(
timelineProtectionState = timelineProtectionState, timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
onClick = {}, onContentClick = {},
onLongClick = {}, onLongClick = {},
onLinkClick = {}, onLinkClick = {},
onUserDataClick = {}, onUserDataClick = {},

View file

@ -114,7 +114,7 @@ fun TimelineItemEventRow(
renderReadReceipts: Boolean, renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean, isLastOutgoingMessage: Boolean,
isHighlighted: Boolean, isHighlighted: Boolean,
onClick: () -> Unit, onContentClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
@ -130,7 +130,8 @@ fun TimelineItemEventRow(
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onContentClick = onContentClick,
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
eventSink = eventSink, eventSink = eventSink,
modifier = contentModifier, modifier = contentModifier,
@ -150,6 +151,12 @@ fun TimelineItemEventRow(
inReplyToClick(inReplyToEventId) inReplyToClick(inReplyToEventId)
} }
val onWholeItemClick = if (event.isWholeContentClickable) {
onContentClick
} else {
{}
}
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier.fillMaxWidth()) {
if (event.groupPosition.isNew()) { if (event.groupPosition.isNew()) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -173,7 +180,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo, timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = onClick, onContentClick = onWholeItemClick,
onLongClick = onLongClick, onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick, inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick, onUserDataClick = ::onUserDataClick,
@ -207,7 +214,7 @@ fun TimelineItemEventRow(
isHighlighted = isHighlighted, isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo, timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = onClick, onContentClick = onWholeItemClick,
onLongClick = onLongClick, onLongClick = onLongClick,
inReplyToClick = ::inReplyToClick, inReplyToClick = ::inReplyToClick,
onUserDataClick = ::onUserDataClick, onUserDataClick = ::onUserDataClick,
@ -263,7 +270,7 @@ private fun TimelineItemEventRowContent(
isHighlighted: Boolean, isHighlighted: Boolean,
timelineRoomInfo: TimelineRoomInfo, timelineRoomInfo: TimelineRoomInfo,
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
onClick: () -> Unit, onContentClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
inReplyToClick: () -> Unit, inReplyToClick: () -> Unit,
onUserDataClick: () -> Unit, onUserDataClick: () -> Unit,
@ -340,7 +347,7 @@ private fun TimelineItemEventRowContent(
}, },
state = bubbleState, state = bubbleState,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = onClick, onClick = onContentClick,
onLongClick = onLongClick, onLongClick = onLongClick,
) { ) {
MessageEventBubbleContent( MessageEventBubbleContent(

View file

@ -57,10 +57,11 @@ fun TimelineItemGroupedEventsRow(
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
eventSink = eventSink, eventSink = eventSink,
modifier = contentModifier, modifier = contentModifier,
onContentClick = {},
onContentLayoutChange = onContentLayoutChange onContentLayoutChange = onContentLayoutChange
) )
}, },
@ -121,10 +122,11 @@ private fun TimelineItemGroupedEventsRowContent(
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
eventSink = eventSink, eventSink = eventSink,
modifier = contentModifier, modifier = contentModifier,
onContentClick = {},
onContentLayoutChange = onContentLayoutChange onContentLayoutChange = onContentLayoutChange
) )
}, },
@ -152,7 +154,7 @@ private fun TimelineItemGroupedEventsRowContent(
focusedEventId = focusedEventId, focusedEventId = focusedEventId,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onClick = onClick, onContentClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
inReplyToClick = inReplyToClick, inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,

View file

@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.text.toPx
import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -44,7 +43,7 @@ internal fun TimelineItemRow(
focusedEventId: EventId?, focusedEventId: EventId?,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit, onLinkClick: (String) -> Unit,
onClick: (TimelineItem.Event) -> Unit, onContentClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit, inReplyToClick: (EventId) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit,
@ -60,7 +59,8 @@ internal fun TimelineItemRow(
TimelineItemEventContentView( TimelineItemEventContentView(
content = event.content, content = event.content,
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onContentClick = { onContentClick(event) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
eventSink = eventSink, eventSink = eventSink,
modifier = contentModifier, modifier = contentModifier,
@ -95,7 +95,7 @@ internal fun TimelineItemRow(
renderReadReceipts = renderReadReceipts, renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId), isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) }, onClick = { onContentClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick, onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink, eventSink = eventSink,
@ -118,11 +118,7 @@ internal fun TimelineItemRow(
timelineProtectionState = timelineProtectionState, timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId), isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = if (timelineProtectionState.hideMediaContent(timelineItem.eventId) && timelineItem.mustBeProtected()) { onContentClick = { onContentClick(timelineItem) },
{}
} else {
{ onClick(timelineItem) }
},
onLongClick = { onLongClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
@ -148,7 +144,7 @@ internal fun TimelineItemRow(
renderReadReceipts = renderReadReceipts, renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
focusedEventId = focusedEventId, focusedEventId = focusedEventId,
onClick = onClick, onClick = onContentClick,
onLongClick = onLongClick, onLongClick = onLongClick,
inReplyToClick = inReplyToClick, inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,

View file

@ -72,8 +72,9 @@ fun TimelineItemStateEventRow(
content = event.content, content = event.content,
onLinkClick = {}, onLinkClick = {},
hideMediaContent = false, hideMediaContent = false,
onShowClick = {}, onShowContentClick = {},
eventSink = eventSink, eventSink = eventSink,
onContentClick = {},
modifier = Modifier.defaultTimelineContentPadding() modifier = Modifier.defaultTimelineContentPadding()
) )
} }

View file

@ -36,7 +36,8 @@ import io.element.android.libraries.architecture.Presenter
fun TimelineItemEventContentView( fun TimelineItemEventContentView(
content: TimelineItemEventContent, content: TimelineItemEventContent,
hideMediaContent: Boolean, hideMediaContent: Boolean,
onShowClick: () -> Unit, onContentClick: () -> Unit,
onShowContentClick: () -> Unit,
onLinkClick: (url: String) -> Unit, onLinkClick: (url: String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -67,25 +68,31 @@ fun TimelineItemEventContentView(
) )
is TimelineItemLocationContent -> TimelineItemLocationView( is TimelineItemLocationContent -> TimelineItemLocationView(
content = content, content = content,
onContentClick = onContentClick,
modifier = modifier modifier = modifier
) )
is TimelineItemImageContent -> TimelineItemImageView( is TimelineItemImageContent -> TimelineItemImageView(
content = content, content = content,
hideMediaContent = hideMediaContent, hideMediaContent = hideMediaContent,
onShowClick = onShowClick, onContentClick = onContentClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onContentLayoutChange = onContentLayoutChange, onContentLayoutChange = onContentLayoutChange,
modifier = modifier, modifier = modifier,
) )
is TimelineItemStickerContent -> TimelineItemStickerView( is TimelineItemStickerContent -> TimelineItemStickerView(
content = content, content = content,
hideMediaContent = hideMediaContent, hideMediaContent = hideMediaContent,
onShowClick = onShowClick, onContentClick = onContentClick,
onShowClick = onShowContentClick,
modifier = modifier, modifier = modifier,
) )
is TimelineItemVideoContent -> TimelineItemVideoView( is TimelineItemVideoContent -> TimelineItemVideoView(
content = content, content = content,
hideMediaContent = hideMediaContent, hideMediaContent = hideMediaContent,
onShowClick = onShowClick, onContentClick = onContentClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onContentLayoutChange = onContentLayoutChange, onContentLayoutChange = onContentLayoutChange,
modifier = modifier modifier = modifier
) )

View file

@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString import android.text.SpannedString
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -50,7 +51,6 @@ import io.element.android.features.messages.impl.timeline.protection.ProtectedVi
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.compose.EditorStyledText
@ -59,7 +59,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText
fun TimelineItemImageView( fun TimelineItemImageView(
content: TimelineItemImageContent, content: TimelineItemImageContent,
hideMediaContent: Boolean, hideMediaContent: Boolean,
onShowClick: () -> Unit, onContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onShowContentClick: () -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -78,13 +80,14 @@ fun TimelineItemImageView(
) { ) {
ProtectedView( ProtectedView(
hideContent = hideMediaContent, hideContent = hideMediaContent,
onShowClick = onShowClick, onShowClick = onShowContentClick,
) { ) {
var isLoaded by remember { mutableStateOf(false) } var isLoaded by remember { mutableStateOf(false) }
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier), .then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.clickable(onClick = onContentClick),
model = content.thumbnailMediaRequestData, model = content.thumbnailMediaRequestData,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
alignment = Alignment.Center, alignment = Alignment.Center,
@ -99,9 +102,7 @@ fun TimelineItemImageView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formattedCaption?.body content.formattedCaption ?: SpannedString(content.caption)
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -114,6 +115,7 @@ fun TimelineItemImageView(
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio), .widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption, text = caption,
style = ElementRichTextEditorStyle.textStyle(), style = ElementRichTextEditorStyle.textStyle(),
onLinkClickedListener = onLinkClick,
releaseOnDetach = false, releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
) )
@ -128,7 +130,9 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon
TimelineItemImageView( TimelineItemImageView(
content = content, content = content,
hideMediaContent = false, hideMediaContent = false,
onShowClick = {}, onShowContentClick = {},
onContentClick = {},
onLinkClick = {},
onContentLayoutChange = {}, onContentLayoutChange = {},
) )
} }
@ -139,7 +143,9 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
TimelineItemImageView( TimelineItemImageView(
content = aTimelineItemImageContent(), content = aTimelineItemImageContent(),
hideMediaContent = true, hideMediaContent = true,
onShowClick = {}, onShowContentClick = {},
onContentClick = {},
onLinkClick = {},
onContentLayoutChange = {}, onContentLayoutChange = {},
) )
} }

View file

@ -7,6 +7,7 @@
package io.element.android.features.messages.impl.timeline.components.event package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
@ -25,9 +26,10 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun TimelineItemLocationView( fun TimelineItemLocationView(
content: TimelineItemLocationContent, content: TimelineItemLocationContent,
onContentClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier.clickable(onClick = onContentClick).fillMaxWidth()) {
content.description?.let { content.description?.let {
Text( Text(
text = it, text = it,
@ -51,5 +53,8 @@ fun TimelineItemLocationView(
@Composable @Composable
internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) =
ElementPreview { ElementPreview {
TimelineItemLocationView(content) TimelineItemLocationView(
content = content,
onContentClick = {},
)
} }

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.timeline.components.event package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -40,6 +41,7 @@ private const val STICKER_SIZE_IN_DP = 128
fun TimelineItemStickerView( fun TimelineItemStickerView(
content: TimelineItemStickerContent, content: TimelineItemStickerContent,
hideMediaContent: Boolean, hideMediaContent: Boolean,
onContentClick: () -> Unit,
onShowClick: () -> Unit, onShowClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -61,7 +63,8 @@ fun TimelineItemStickerView(
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier), .then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.clickable(onClick = onContentClick),
model = MediaRequestData( model = MediaRequestData(
source = content.preferredMediaSource, source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
@ -85,6 +88,7 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke
TimelineItemStickerView( TimelineItemStickerView(
content = content, content = content,
hideMediaContent = false, hideMediaContent = false,
onContentClick = {},
onShowClick = {}, onShowClick = {},
) )
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString import android.text.SpannedString
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -56,7 +57,6 @@ import io.element.android.libraries.designsystem.components.blurhash.blurHashBac
import io.element.android.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.modifiers.roundedBackground
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
@ -68,7 +68,9 @@ import io.element.android.wysiwyg.compose.EditorStyledText
fun TimelineItemVideoView( fun TimelineItemVideoView(
content: TimelineItemVideoContent, content: TimelineItemVideoContent,
hideMediaContent: Boolean, hideMediaContent: Boolean,
onShowClick: () -> Unit, onContentClick: () -> Unit,
onShowContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -90,13 +92,14 @@ fun TimelineItemVideoView(
) { ) {
ProtectedView( ProtectedView(
hideContent = hideMediaContent, hideContent = hideMediaContent,
onShowClick = onShowClick, onShowClick = onShowContentClick,
) { ) {
var isLoaded by remember { mutableStateOf(false) } var isLoaded by remember { mutableStateOf(false) }
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier), .then(if (isLoaded) Modifier.background(Color.White) else Modifier)
.clickable(onClick = onContentClick),
model = MediaRequestData( model = MediaRequestData(
source = content.thumbnailSource, source = content.thumbnailSource,
kind = MediaRequestData.Kind.Thumbnail( kind = MediaRequestData.Kind.Thumbnail(
@ -128,9 +131,7 @@ fun TimelineItemVideoView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formattedCaption?.body content.formattedCaption ?: SpannedString(content.caption)
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -142,6 +143,7 @@ fun TimelineItemVideoView(
.padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio), .widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption, text = caption,
onLinkClickedListener = onLinkClick,
style = ElementRichTextEditorStyle.textStyle(), style = ElementRichTextEditorStyle.textStyle(),
releaseOnDetach = false, releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange), onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
@ -157,7 +159,9 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon
TimelineItemVideoView( TimelineItemVideoView(
content = content, content = content,
hideMediaContent = false, hideMediaContent = false,
onShowClick = {}, onShowContentClick = {},
onContentClick = {},
onLinkClick = {},
onContentLayoutChange = {}, onContentLayoutChange = {},
) )
} }
@ -168,7 +172,9 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
TimelineItemVideoView( TimelineItemVideoView(
content = aTimelineItemVideoContent(), content = aTimelineItemVideoContent(),
hideMediaContent = true, hideMediaContent = true,
onShowClick = {}, onShowContentClick = {},
onContentClick = {},
onLinkClick = {},
onContentLayoutChange = {}, onContentLayoutChange = {},
) )
} }

View file

@ -86,7 +86,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemImageContent( TimelineItemImageContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -105,7 +105,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemStickerContent( TimelineItemStickerContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -142,7 +142,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemVideoContent( TimelineItemVideoContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -161,7 +161,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemAudioContent( TimelineItemAudioContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -176,7 +176,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
eventId = eventId, eventId = eventId,
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -187,7 +187,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemAudioContent( TimelineItemAudioContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -202,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
TimelineItemFileContent( TimelineItemFileContent(
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(), caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption, formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source, fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),

View file

@ -9,8 +9,10 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent 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.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -93,6 +95,17 @@ sealed interface TimelineItem {
val isRemote = eventId != null val isRemote = eventId != null
/** Whether a click on any part of the event bubble should trigger the 'onContentClick' callback.
*
* This is `true` for all events except for visual media events with a caption or formatted caption.
*/
val isWholeContentClickable = when (content) {
is TimelineItemStickerContent -> content.formattedCaption == null && content.caption == null
is TimelineItemImageContent -> content.formattedCaption == null && content.caption == null
is TimelineItemVideoContent -> content.formattedCaption == null && content.caption == null
else -> true
}
val eventOrTransactionId: EventOrTransactionId val eventOrTransactionId: EventOrTransactionId
get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId) get() = EventOrTransactionId.from(eventId = eventId, transactionId = transactionId)

View file

@ -8,14 +8,13 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemAudioContent( data class TimelineItemAudioContent(
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
@Immutable @Immutable
sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContent {
@ -19,7 +18,7 @@ sealed interface TimelineItemEventContent {
sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent { sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent {
val filename: String val filename: String
val caption: String? val caption: String?
val formattedCaption: FormattedBody? val formattedCaption: CharSequence?
val bestDescription: String val bestDescription: String
get() = caption ?: filename get() = caption ?: filename

View file

@ -8,13 +8,12 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
data class TimelineItemFileContent( data class TimelineItemFileContent(
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val fileSource: MediaSource, val fileSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,

View file

@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
@ -17,7 +16,7 @@ import io.element.android.libraries.matrix.ui.media.MediaRequestData
data class TimelineItemImageContent( data class TimelineItemImageContent(
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,

View file

@ -8,12 +8,11 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
data class TimelineItemStickerContent( data class TimelineItemStickerContent(
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,

View file

@ -8,13 +8,12 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemVideoContent( data class TimelineItemVideoContent(
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val duration: Duration, val duration: Duration,
val videoSource: MediaSource, val videoSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,

View file

@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration import kotlin.time.Duration
@ -17,7 +16,7 @@ data class TimelineItemVoiceContent(
val eventId: EventId?, val eventId: EventId?,
override val filename: String, override val filename: String,
override val caption: String?, override val caption: String?,
override val formattedCaption: FormattedBody?, override val formattedCaption: CharSequence?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,

View file

@ -529,7 +529,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
state = state, state = state,
onBackClick = onBackClick, onBackClick = onBackClick,
onRoomDetailsClick = onRoomDetailsClick, onRoomDetailsClick = onRoomDetailsClick,
onEventClick = onEventClick, onEventContentClick = onEventClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onPreviewAttachments = onPreviewAttachments, onPreviewAttachments = onPreviewAttachments,

View file

@ -158,7 +158,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimel
timelineProtectionState = timelineProtectionState, timelineProtectionState = timelineProtectionState,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick, onLinkClick = onLinkClick,
onMessageClick = onMessageClick, onContentClick = onMessageClick,
onMessageLongClick = onMessageLongClick, onMessageLongClick = onMessageLongClick,
onSwipeToReply = onSwipeToReply, onSwipeToReply = onSwipeToReply,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,

View file

@ -286,7 +286,7 @@ class TimelineItemContentMessageFactoryTest {
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
filename = "body.mp4", filename = "body.mp4",
caption = "body.mp4 caption", caption = "body.mp4 caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"), formattedCaption = SpannedString("formatted"),
duration = 1.minutes, duration = 1.minutes,
videoSource = MediaSource(url = "url", json = null), videoSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
@ -527,7 +527,7 @@ class TimelineItemContentMessageFactoryTest {
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
filename = "body.jpg", filename = "body.jpg",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"), formattedCaption = SpannedString("formatted"),
caption = "body.jpg caption", caption = "body.jpg caption",
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),

View file

@ -26,7 +26,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Optional import java.util.Optional
class RoomAliasResolverPresenterTest { class RoomAliasHelperPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()

View file

@ -27,7 +27,7 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RoomAliasResolverViewTest { class RoomAliasHelperViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test @Test

View file

@ -145,7 +145,7 @@ private fun RoomDirectoryRoomList(
Text( Text(
text = stringResource(id = CommonStrings.common_no_results), text = stringResource(id = CommonStrings.common_no_results),
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPlaceholder, color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
} }
@ -185,8 +185,8 @@ private fun SearchTextField(
colors: TextFieldColors = TextFieldDefaults.colors( colors: TextFieldColors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder, unfocusedPlaceholderColor = ElementTheme.colors.textSecondary,
focusedPlaceholderColor = ElementTheme.colors.textPlaceholder, focusedPlaceholderColor = ElementTheme.colors.textSecondary,
focusedTextColor = ElementTheme.colors.textPrimary, focusedTextColor = ElementTheme.colors.textPrimary,
unfocusedTextColor = ElementTheme.colors.textPrimary, unfocusedTextColor = ElementTheme.colors.textPrimary,
focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,

View file

@ -41,7 +41,7 @@ fun NumberedListMolecule(
private fun ItemNumber( private fun ItemNumber(
index: Int, index: Int,
) { ) {
val color = ElementTheme.colors.textPlaceholder val color = ElementTheme.colors.textSecondary
Box( Box(
modifier = Modifier modifier = Modifier
.border(1.dp, color, CircleShape) .border(1.dp, color, CircleShape)

View file

@ -200,7 +200,7 @@ private fun DecorationBox(
if (placeholder != null && isTextEmpty) { if (placeholder != null && isTextEmpty) {
Text( Text(
text = placeholder, text = placeholder,
color = ElementTheme.colors.textPlaceholder, color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
) )
} }

View file

@ -168,3 +168,13 @@ fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<Ma
.map { roomSummary -> roomSummary.map { it.info } } .map { roomSummary -> roomSummary.map { it.info } }
.distinctUntilChanged() .distinctUntilChanged()
} }
/**
* Returns a room alias from a room alias name.
* @param name the room alias name ie. the local part of the room alias.
*/
fun MatrixClient.roomAliasFromName(name: String): Result<RoomAlias> {
return runCatching {
RoomAlias("#$name:${userIdServerName()}")
}
}

View file

@ -20,5 +20,5 @@ data class CreateRoomParameters(
val invite: List<UserId>? = null, val invite: List<UserId>? = null,
val avatar: String? = null, val avatar: String? = null,
val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None, val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None,
val canonicalAlias: Optional<String> = Optional.empty(), val roomAliasName: Optional<String> = Optional.empty(),
) )

View file

@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomAlias
interface RoomAliasHelper {
fun roomAliasNameFromRoomDisplayName(name: String): String
fun isRoomAliasValid(roomAlias: RoomAlias): Boolean
}

View file

@ -334,7 +334,7 @@ class RustMatrixClient(
JoinRuleOverride.Knock -> RustJoinRule.Knock JoinRuleOverride.Knock -> RustJoinRule.Knock
JoinRuleOverride.None -> null JoinRuleOverride.None -> null
}, },
canonicalAlias = createRoomParams.canonicalAlias.getOrNull(), canonicalAlias = createRoomParams.roomAliasName.getOrNull(),
) )
val roomId = RoomId(client.createRoom(rustParams)) val roomId = RoomId(client.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails. // Wait to receive the room back from the sync but do not returns failure if it fails.

View file

@ -0,0 +1,25 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.alias
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomAliasHelper @Inject constructor() : RoomAliasHelper {
override fun roomAliasNameFromRoomDisplayName(name: String): String {
return org.matrix.rustcomponents.sdk.roomAliasNameFromRoomDisplayName(name)
}
override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean {
return org.matrix.rustcomponents.sdk.isRoomAliasFormatValid(roomAlias.value)
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.room.alias
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
class FakeRoomAliasHelper(
private val roomAliasNameFromRoomDisplayNameLambda: (String) -> String = { name ->
name.trimStart().trimEnd().replace(" ", "_")
},
private val isRoomAliasValidLambda: (RoomAlias) -> Boolean = { true }
) : RoomAliasHelper {
override fun roomAliasNameFromRoomDisplayName(name: String): String {
return roomAliasNameFromRoomDisplayNameLambda(name)
}
override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean {
return isRoomAliasValidLambda(roomAlias)
}
}

View file

@ -32,10 +32,8 @@ val localAarProjects = listOf(
val excludedKoverSubProjects = listOf( val excludedKoverSubProjects = listOf(
":app", ":app",
":samples",
":anvilannotations", ":anvilannotations",
":anvilcodegen", ":anvilcodegen",
":samples:minimal",
":tests:testutils", ":tests:testutils",
// Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix
// SDK api, so it is not really relevant to unit test it: there is no logic to test. // SDK api, so it is not really relevant to unit test it: there is no logic to test.

View file

@ -6,7 +6,7 @@
*/ */
/** /**
* This will generate the plugin "io.element.android-compose-application" to use by app and samples modules * This will generate the plugin "io.element.android-compose-application" to use by app
*/ */
import extension.androidConfig import extension.androidConfig
import extension.commonDependencies import extension.commonDependencies

View file

@ -1 +0,0 @@
/build

View file

@ -1,67 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
plugins {
id("io.element.android-compose-application")
alias(libs.plugins.kotlin.android)
}
android {
namespace = "io.element.android.samples.minimal"
defaultConfig {
applicationId = "io.element.android.samples.minimal"
targetSdk = Versions.TARGET_SDK
versionCode = Versions.VERSION_CODE
versionName = Versions.VERSION_NAME
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.preference)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.sessionStorage.implMemory)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.network)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.eventformatter.impl)
implementation(projects.libraries.fullscreenintent.impl)
implementation(projects.libraries.preferences.impl)
implementation(projects.libraries.preferences.test)
implementation(projects.libraries.indicator.impl)
implementation(projects.features.invite.impl)
implementation(projects.features.roomlist.impl)
implementation(projects.features.leaveroom.impl)
implementation(projects.features.login.impl)
implementation(projects.features.logout.impl)
implementation(projects.features.networkmonitor.impl)
implementation(projects.services.toolbox.impl)
implementation(projects.libraries.featureflag.impl)
implementation(projects.services.analytics.noop)
implementation(libs.coroutines.core)
implementation(projects.libraries.push.test)
}

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ElementX">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ElementX">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,23 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class AlwaysEnabledFeatureFlagService : FeatureFlagService {
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
return flowOf(true)
}
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
return true
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
class LoginScreen(private val authenticationService: MatrixAuthenticationService) {
@Composable
fun Content(modifier: Modifier = Modifier) {
val presenter = remember {
LoginPasswordPresenter(
authenticationService = authenticationService,
AccountProviderDataSource(),
DefaultLoginUserStory(),
)
}
LaunchedEffect(Unit) {
authenticationService.setHomeserver(defaultAccountProvider.url)
}
val state = presenter.present()
LoginPasswordView(
state = state,
modifier = modifier,
onBackClick = {},
)
}
}

View file

@ -1,95 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.impl.RustClientBuilderProvider
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.OidcConfigurationProvider
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.impl.room.RustTimelineEventTypeFilterFactory
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import kotlinx.coroutines.runBlocking
import java.io.File
class MainActivity : ComponentActivity() {
private val matrixAuthenticationService: MatrixAuthenticationService by lazy {
val baseDirectory = File(applicationContext.filesDir, "sessions")
val userAgentProvider = SimpleUserAgentProvider("MinimalSample")
val sessionStore = InMemorySessionStore()
val userCertificatesProvider = NoOpUserCertificatesProvider()
val proxyProvider = NoOpProxyProvider()
RustMatrixAuthenticationService(
sessionPathsFactory = SessionPathsFactory(baseDirectory, applicationContext.cacheDir),
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
rustMatrixClientFactory = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = applicationContext.cacheDir,
appCoroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
userAgentProvider = userAgentProvider,
userCertificatesProvider = userCertificatesProvider,
proxyProvider = proxyProvider,
clock = DefaultSystemClock(),
analyticsService = NoopAnalyticsService(),
featureFlagService = AlwaysEnabledFeatureFlagService(),
timelineEventTypeFilterFactory = RustTimelineEventTypeFilterFactory(),
clientBuilderProvider = RustClientBuilderProvider(),
),
passphraseGenerator = NullPassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
appPreferencesStore = InMemoryAppPreferencesStore(),
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ElementTheme {
val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn)
Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize())
}
}
}
@Composable
fun Content(
isLoggedIn: Boolean,
modifier: Modifier = Modifier
) {
if (!isLoggedIn) {
LoginScreen(authenticationService = matrixAuthenticationService).Content(modifier)
} else {
val matrixClient = runBlocking {
val sessionId = matrixAuthenticationService.getLatestSessionId()!!
matrixAuthenticationService.restoreSession(sessionId).getOrNull()
}
RoomListScreen(LocalContext.current, matrixClient!!).Content(modifier)
}
}
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
class NoOpProxyProvider : ProxyProvider {
override fun provides(): String? = null
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
class NoOpUserCertificatesProvider : UserCertificatesProvider {
override fun provides(): List<ByteArray> = emptyList()
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
class NullPassphraseGenerator : PassphraseGenerator {
override fun generatePassphrase(): String? = null
}

View file

@ -1,18 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.net.Uri
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
class OnlyFallbackPermalinkParser : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.FallbackLink(Uri.parse(uriString))
}
}

View file

@ -1,174 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
import io.element.android.features.logout.impl.direct.DirectLogoutPresenter
import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListView
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
import io.element.android.libraries.androidutils.system.DefaultDateTimeObserver
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.impl.DateFormatters
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFormatter
import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter
import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter
import io.element.android.libraries.eventformatter.impl.StateContentFormatter
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.impl.room.join.DefaultJoinRoom
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import timber.log.Timber
import java.util.Locale
class RoomListScreen(
context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers = Singleton.coroutineDispatchers,
) {
private val clock = Clock.System
private val locale = Locale.getDefault()
private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() }
private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() }
private val sessionVerificationService = matrixClient.sessionVerificationService()
private val encryptionService = matrixClient.encryptionService()
private val stringProvider = AndroidStringProvider(context.resources)
private val featureFlagService = AlwaysEnabledFeatureFlagService()
private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(
localDateTimeProvider = dateTimeProvider,
dateFormatters = dateFormatters
),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
sp = stringProvider,
roomMembershipContentFormatter = RoomMembershipContentFormatter(
matrixClient = matrixClient,
sp = stringProvider
),
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
stateContentFormatter = StateContentFormatter(stringProvider),
permalinkParser = OnlyFallbackPermalinkParser(),
),
)
private val presenter = RoomListPresenter(
client = matrixClient,
networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(),
leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource(
roomListService = matrixClient.roomListService,
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers,
notificationSettingsService = matrixClient.notificationSettingsService(),
appScope = Singleton.appScope,
dateTimeObserver = DefaultDateTimeObserver(context),
),
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService,
),
featureFlagService = featureFlagService,
searchPresenter = RoomListSearchPresenter(
RoomListSearchDataSource(
roomListService = matrixClient.roomListService,
roomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers,
),
featureFlagService = featureFlagService,
),
sessionPreferencesStore = DefaultSessionPreferencesStore(
context = context,
sessionId = matrixClient.sessionId,
sessionCoroutineScope = Singleton.appScope
),
filtersPresenter = RoomListFiltersPresenter(
roomListService = matrixClient.roomListService,
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
),
acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter(
client = matrixClient,
joinRoom = DefaultJoinRoom(matrixClient, NoopAnalyticsService()),
notificationCleaner = FakeNotificationCleaner(),
),
analyticsService = NoopAnalyticsService(),
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
notificationCleaner = FakeNotificationCleaner(),
logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService),
)
@Composable
fun Content(modifier: Modifier = Modifier) {
fun onRoomClick(roomId: RoomId) {
Singleton.appScope.launch {
withContext(coroutineDispatchers.io) {
matrixClient.getRoom(roomId)!!.use { room ->
room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS)
}
}
}
}
val state = presenter.present()
RoomListView(
state = state,
onRoomClick = ::onRoomClick,
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onCreateRoomClick = {},
onRoomSettingsClick = {},
onMenuActionClick = {},
onRoomDirectorySearchClick = {},
modifier = modifier,
acceptDeclineInviteView = {
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
},
onMigrateToNativeSlidingSyncClick = {},
)
DisposableEffect(Unit) {
Timber.w("Start sync!")
runBlocking {
matrixClient.syncService().startSync()
}
onDispose {
Timber.w("Stop sync!")
runBlocking {
matrixClient.syncService().stopSync()
}
}
}
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.libraries.matrix.impl.tracing.RustTracingService
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
object Singleton {
val buildMeta = BuildMeta(
isDebuggable = true,
buildType = BuildType.DEBUG,
applicationName = "EAX-Minimal",
productionApplicationName = "EAX-Minimal",
desktopApplicationName = "EAX-Minimal-Desktop",
applicationId = "io.element.android.samples.minimal",
isEnterpriseBuild = false,
lowPrivacyLoggingEnabled = false,
versionName = "0.1.0",
versionCode = 1,
gitRevision = "",
gitBranchName = "",
flavorDescription = "NA",
flavorShortDescription = "NA",
)
init {
val tracingConfiguration = TracingConfiguration(
filterConfiguration = TracingFilterConfigurations.debug,
writesToLogcat = true,
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
)
RustTracingService(buildMeta).setupTracing(tracingConfiguration)
}
val appScope = MainScope() + CoroutineName("Minimal Scope")
val coroutineDispatchers = CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.NoActionBar" />
</resources>

View file

@ -1,9 +0,0 @@
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<string name="app_name">EAX-Sample</string>
</resources>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View file

@ -70,8 +70,6 @@ include(":tests:testutils")
include(":anvilannotations") include(":anvilannotations")
include(":anvilcodegen") include(":anvilcodegen")
include(":samples:minimal")
fun includeProjects(directory: File, path: String, maxDepth: Int = 1) { fun includeProjects(directory: File, path: String, maxDepth: Int = 1) {
directory.listFiles().orEmpty().also { it.sort() }.forEach { file -> directory.listFiles().orEmpty().also { it.sort() }.forEach { file ->
if (file.isDirectory) { if (file.isDirectory) {

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c94e5e7c3b044017e6cb98985671875c7ab6103533e2a5e15aed17a01a52576b oid sha256:3b380df8a4e69e9e11337db374b682e01f96738948e69eb33886817894a049b0
size 30470 size 30716

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e6270f3b7935c47da76ce4e81aa7e8eac13a9340e7ccb198f8cd6956d746dfa8 oid sha256:ffbdcd28c32808fba8ffb7071b6440578716d3ae89149eb319c22d3f3b78b1ff
size 59452 size 59499

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f6a98ba9f5edc21b8781e84ccaf6f93c3608a4aa64464494304962d8cb62a102 oid sha256:9dc23b7e9533aa94245efa2089956259b00882809157cf32decaaa22827aeb59
size 31477 size 31678

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c3b02b0bc0d557bac4d5aa945daf04d5d58dab44d9e4284d030804e6b5ed6a49 oid sha256:9dc54d0a6c3cb3069482b1c27ed6d80fd63b0bcad1a550bf5c4edb09f69db3a7
size 60703 size 60749

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:91cdcd831bc82480a130093a2f561bde8f0c3f0f2b9e7251628763202fc7bc39 oid sha256:ad1a15591ad4c27119d50e14869314371a87e3afe7bd6757bf245f27c99ef00c
size 35249 size 35246

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:971e260530cfd5b25cce035272820cb067c7e4a4a34fa6992526e5176913ea2d oid sha256:4a63c918393297e53f1a54b8de150ae807662ac905d5112d43812665f5a8f0ef
size 36617 size 36647

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:56fde0cdb5a5196c277c43a1b9909401f40a4e7ef8bfe2e4d80592f0768fe5d5 oid sha256:59bf5b8ed10344e299e5a8b977f3bc21915c500c7b79cfd44bdcc2a8a134fbdc
size 27306 size 27291

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2db3dad746069968a83693752e985c25b72ca31be11b5c65d7da234f96d377d3 oid sha256:0a3339897e11dbdb7d1fa99cf208d467dc19db03599cde5e6ba21d7b19c522de
size 34055 size 34193

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:61739bd834ee18df778b4737f38f656d4342397135d518791ff4518d8fdea96d oid sha256:116c5f660aca1866667814903df752ae522c4dcdee2478c7d6815bc5f427684e
size 35173 size 35317

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ae8a729e91657959dbac49ef1c0f41fe37c296923eef6c22bdc9692d389ca346 oid sha256:30ac5683fe4cc16bd6e31745b9f479a1b1d5e2a1dcf3f18ec7bd5f7793b8fda9
size 25207 size 25273

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c66e45979657915aa7010b4796bf80865e8cb96118888c78d86e9edf0fa9bb88 oid sha256:b738607cad294fea5617f81e4d410896704c395ffcf4118c25228e1d4ab693ba
size 66346 size 66436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:341782379e27258de391ba5d9713172eda666a593ca3040c9cfff96aaff72825 oid sha256:d35646ae638353674a985215cc521db18e8ae6df7de3d241767ad5c994985814
size 64908 size 65036

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:788722dec315d910767e187eb6c868fb85918d0220c7b923fed82fae736c7c38 oid sha256:b1b50c558703a1fb83dfd8b4dcc8484fc3aaf019e4ab466f0e515db401f0797d
size 49947 size 49999

Some files were not shown because too many files have changed in this diff Show more