Merge branch 'develop' into renovate/compose.bom
5
.github/workflows/build.yml
vendored
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t 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 shouldn’t be more than 3 lines",
|
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t 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 shouldn’t 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 shouldn’t 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 shouldn’t 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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview(
|
||||||
),
|
),
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onRoomDetailsClick = {},
|
onRoomDetailsClick = {},
|
||||||
onEventClick = { false },
|
onEventContentClick = { false },
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onPreviewAttachments = {},
|
onPreviewAttachments = {},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
|
||||||
timelineProtectionState = aTimelineProtectionState(),
|
timelineProtectionState = aTimelineProtectionState(),
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
onMessageClick = {},
|
onContentClick = {},
|
||||||
onMessageLongClick = {},
|
onMessageLongClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
1
samples/minimal/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ccbb514ad9bd6a95ecff2e278574ac5bff9bd8a63a5fc91fa35302dbaa9e71f2
|
||||||
|
size 55276
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a2ea9839e3dd1288d99c2115c37bc7fe72716af9785c4de3e328a77a57d12d70
|
||||||
|
size 56633
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5b28da0e8ff5cbedf8777b5af8ff6bab2bd5c476df80878b2c28dd96a354d978
|
||||||
|
size 54383
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:71c42c595070733ac8ac2c50da116e73ea6eea7dc120f6f0d28022b5a5f925bd
|
||||||
|
size 57099
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e10c1c3f2b4045d20c9513c70bc4ea041e091976df5385b93d45c2d03b8f6e5f
|
||||||
|
size 58513
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cb54ae79de75af32e32642063d9dc9e0c24b0089f187a043f0fc330151c72c75
|
||||||
|
size 56170
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||