Merge branch 'release/25.07.0' into main

This commit is contained in:
Benoit Marty 2025-07-04 16:50:22 +02:00
commit 86ec2f5ea5
1075 changed files with 9425 additions and 3995 deletions

View file

@ -270,7 +270,6 @@ jobs:
- name: Run shellcheck
uses: ludeeus/action-shellcheck@2.0.0
with:
scandir: ./tools
severity: warning
upload_reports:

View file

@ -12,8 +12,8 @@ mkdir -p /data/local/tmp/recordings;
FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4
while true
do
((COUNT++))
COUNT=$((COUNT+1))
FILENAME=/data/local/tmp/recordings/testRecording$COUNT.mp4
echo "\nRecording video file #$COUNT"
printf "\nRecording video file #%d\n" $COUNT
screenrecord --bugreport --bit-rate=16m --size 720x1280 $FILENAME
done

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.21" />
<option name="version" value="2.2.0" />
</component>
</project>

View file

@ -1,3 +1,45 @@
Changes in Element X v25.06.3
=============================
## What's Changed
### ✨ Features
* Feature : room version upgrade by @ganfra in https://github.com/element-hq/element-x-android/pull/4862
* Add a developer option for history sharing on invite by @richvdh in https://github.com/element-hq/element-x-android/pull/4821
### 🙌 Improvements
* Change : add tombstoned room decoration by @ganfra in https://github.com/element-hq/element-x-android/pull/4891
* Show generic notification when Event cannot be resolved by @bmarty in https://github.com/element-hq/element-x-android/pull/4889
### 🐛 Bugfixes
* [a11y] Improve screen reader on polls by @bmarty in https://github.com/element-hq/element-x-android/pull/4875
* fix (event action): allow to edit only if permission to send message by @ganfra in https://github.com/element-hq/element-x-android/pull/4895
* fix (room upgrade) : room predecessor banner on DM room by @ganfra in https://github.com/element-hq/element-x-android/pull/4896
* fix (join room) : do not navigate up when join is successful by @ganfra in https://github.com/element-hq/element-x-android/pull/4899
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4842
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4881
### Dependency upgrades
* chore(deps): update plugin dependencycheck to v12.1.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4856
* fix(deps): update dependency org.maplibre.gl:android-sdk to v11.10.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4858
* fix(deps): update kotlin to v2.1.21-2.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4850
* fix(deps): update dependency app.cash.turbine:turbine to v1.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4865
* Update dependency com.posthog:posthog-android to v3.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4873
* Update dependency org.maplibre.gl:android-sdk to v11.10.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4879
* fix(deps): update dependency com.posthog:posthog-android to v3.19.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4882
* fix(deps): update dependency io.sentry:sentry-android to v8.13.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4870
* fix(deps): update showkase to v1.0.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4878
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.6.18 by @renovate in https://github.com/element-hq/element-x-android/pull/4894
### Others
* Annotate Composable functions with `@ReadOnlyComposable` where it's possible by @bmarty in https://github.com/element-hq/element-x-android/pull/4859
* Add documentation on WebViewPipController by @bmarty in https://github.com/element-hq/element-x-android/pull/4861
* Small cleanup around log tag. by @bmarty in https://github.com/element-hq/element-x-android/pull/4860
* Another cleanup by @bmarty in https://github.com/element-hq/element-x-android/pull/4869
* Disable BT audio devices for Element Call on Android < 12 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4876
* Add a banner to ask the user to disable battery optimization when Event cannot be resolved from Push by @bmarty in https://github.com/element-hq/element-x-android/pull/4845
* a11y: improve accessibility on rich text editor options. by @bmarty in https://github.com/element-hq/element-x-android/pull/4886
* A11Y: improve accessibility on event reactions. by @bmarty in https://github.com/element-hq/element-x-android/pull/4877
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.06.2...v25.06.3
Changes in Element X v25.06.2
=============================

View file

@ -24,6 +24,7 @@ Learn more about why we are building Element X in our blog post: [https://elemen
* [Translations](#translations)
* [Rust SDK](#rust-sdk)
* [Status](#status)
* [Minimum SDK version](#minimum-sdk-version)
* [Contributing](#contributing)
* [Build instructions](#build-instructions)
* [Support](#support)
@ -73,6 +74,12 @@ We're doing this as a way to share code between platforms and while we've seen p
This project is in an early rollout and migration phase.
## Minimum SDK version
Element X Android requires a minimum SDK version of 24 (Android 7.0, Nougat). We aim to support devices running Android 7.0 and above, which covers a wide range of devices still in use today.
Element Android Enterprise requires a minimum SDK version of 33 (Android 13, Tiramisu). For Element Enterprise, we support only devices that still receive security updates, which means devices running Android 13 and above. Android does not have a documented support policy, but some information can be found at [https://endoflife.date/android](https://endoflife.date/android).
## Contributing
Want to get actively involved in the project? You're more than welcome! A good way to start is to check the issues that are labelled with the [good first issue](https://github.com/element-hq/element-x-android/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label. Let us know by commenting the issue that you're starting working on it.

View file

@ -4,6 +4,7 @@
<locale android:name="bg"/>
<locale android:name="cs"/>
<locale android:name="cy"/>
<locale android:name="da"/>
<locale android:name="de"/>
<locale android:name="el"/>
<locale android:name="en"/>

View file

@ -41,6 +41,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.loggedin.MediaPreviewConfigMigration
import io.element.android.appnav.loggedin.SendQueues
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
@ -49,11 +50,11 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
@ -90,6 +91,15 @@ import java.time.Duration
import java.time.Instant
import java.util.Optional
import java.util.UUID
import kotlin.collections.List
import kotlin.collections.any
import kotlin.collections.emptyList
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.mapNotNull
import kotlin.collections.plus
import kotlin.collections.setOf
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
@ -98,7 +108,7 @@ import kotlin.time.toKotlinDuration
class LoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val roomListEntryPoint: RoomListEntryPoint,
private val homeEntryPoint: HomeEntryPoint,
private val preferencesEntryPoint: PreferencesEntryPoint,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
@ -114,6 +124,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val sendingQueue: SendQueues,
private val logoutEntryPoint: LogoutEntryPoint,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
@ -160,7 +171,7 @@ class LoggedInFlowNode @AssistedInject constructor(
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) {
backstack.elements.first { elements ->
elements.any { it.key.navTarget == NavTarget.RoomList }
elements.any { it.key.navTarget == NavTarget.Home }
}
}
@ -179,13 +190,14 @@ class LoggedInFlowNode @AssistedInject constructor(
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService().setListener(verificationListener)
mediaPreviewConfigMigration()
ftueService.state
.onEach { ftueState ->
when (ftueState) {
is FtueState.Unknown -> Unit // Nothing to do
is FtueState.Incomplete -> backstack.safeRoot(NavTarget.Ftue)
is FtueState.Complete -> backstack.safeRoot(NavTarget.RoomList)
is FtueState.Complete -> backstack.safeRoot(NavTarget.Home)
}
}
.launchIn(lifecycleScope)
@ -212,7 +224,7 @@ class LoggedInFlowNode @AssistedInject constructor(
data object LoggedInPermanent : NavTarget
@Parcelize
data object RoomList : NavTarget
data object Home : NavTarget
@Parcelize
data class Room(
@ -269,8 +281,8 @@ class LoggedInFlowNode @AssistedInject constructor(
}
createNode<LoggedInNode>(buildContext, listOf(callback))
}
NavTarget.RoomList -> {
val callback = object : RoomListEntryPoint.Callback {
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun onRoomClick(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
@ -303,7 +315,7 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
}
}
roomListEntryPoint
homeEntryPoint
.nodeBuilder(this, buildContext)
.callback(callback)
.build()
@ -487,7 +499,7 @@ class LoggedInFlowNode @AssistedInject constructor(
clearBackstack: Boolean,
) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
navTarget is NavTarget.Home
}
attachChild<RoomFlowNode> {
val roomNavTarget = NavTarget.Room(
@ -504,7 +516,7 @@ class LoggedInFlowNode @AssistedInject constructor(
suspend fun attachUser(userId: UserId) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
@ -517,7 +529,7 @@ class LoggedInFlowNode @AssistedInject constructor(
internal suspend fun attachIncomingShare(intent: Intent) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
@ -555,7 +567,7 @@ private class AttachRoomOperation(
return if (clearBackstack) {
// Makes sure the room list target is alone in the backstack and stashed
elements.mapNotNull { element ->
if (element.key.navTarget == LoggedInFlowNode.NavTarget.RoomList) {
if (element.key.navTarget == LoggedInFlowNode.NavTarget.Home) {
element.transitionTo(STASHED, this)
} else {
null

View file

@ -0,0 +1,58 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.loggedin
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
* This migration is temporary, will be safe to remove after some time.
* The goal is to set the server config if it's not set, and remove the local data.
*/
class MediaPreviewConfigMigration @Inject constructor(
private val mediaPreviewService: MediaPreviewService,
private val appPreferencesStore: AppPreferencesStore,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
) {
@Suppress("DEPRECATION")
operator fun invoke() = sessionCoroutineScope.launch {
val hideInviteAvatars = appPreferencesStore.getHideInviteAvatarsFlow().first()
val mediaPreviewValue = appPreferencesStore.getTimelineMediaPreviewValueFlow().first()
if (hideInviteAvatars == null && mediaPreviewValue == null) {
// No local data, abort.
return@launch
}
mediaPreviewService
.fetchMediaPreviewConfig()
.onSuccess { config ->
if (config != null) {
appPreferencesStore.setHideInviteAvatars(null)
appPreferencesStore.setTimelineMediaPreviewValue(null)
} else {
if (hideInviteAvatars != null) {
mediaPreviewService.setHideInviteAvatars(hideInviteAvatars)
appPreferencesStore.setHideInviteAvatars(null)
}
if (mediaPreviewValue != null) {
mediaPreviewService.setMediaPreviewValue(mediaPreviewValue)
appPreferencesStore.setTimelineMediaPreviewValue(null)
}
}
}
.onFailure {
Timber.e(it, "Couldn't perform migration, failed to fetch media preview config.")
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Log ud og opgradér"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s understøtter ikke længere den gamle protokol. Log ud og log ind igen for at fortsætte med at bruge appen."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Din hjemmeserver understøtter ikke længere den gamle protokol. Log ud og log ind igen for at fortsætte med at bruge appen."</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Keluar &amp; Tingkatkan"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Homeserver Anda tidak lagi mendukung protokol lama. Silakan keluar dan masuk kembali untuk terus menggunakan aplikasi."</string>
</resources>

View file

@ -0,0 +1,162 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("DEPRECATION")
package io.element.android.appnav.loggedin
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.media.MediaPreviewConfig
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
import io.element.android.libraries.matrix.test.media.FakeMediaPreviewService
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MediaPreviewConfigMigrationTest {
@Test
fun `when no local data exists, migration does nothing`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore()
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.success(null) }
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify no calls were made to set server config
// since there's nothing to migrate
}
@Test
fun `when local data exists and server has config, clears local data`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setHideInviteAvatars(true)
setTimelineMediaPreviewValue(MediaPreviewValue.Private)
}
val serverConfig = MediaPreviewConfig(
hideInviteAvatar = false,
mediaPreviewValue = MediaPreviewValue.On
)
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.success(serverConfig) }
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify local data was cleared
assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull()
assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull()
}
@Test
fun `when local hideInviteAvatars exists and server has no config, migrates to server`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setHideInviteAvatars(true)
}
var setHideInviteAvatarsValue: Boolean? = null
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.success(null) },
setHideInviteAvatarsResult = { value ->
setHideInviteAvatarsValue = value
Result.success(Unit)
}
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify server was updated with local value
assertThat(setHideInviteAvatarsValue).isTrue()
// Verify local data was cleared
assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull()
}
@Test
fun `when local mediaPreviewValue exists and server has no config, migrates to server`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setTimelineMediaPreviewValue(MediaPreviewValue.Private)
}
var setMediaPreviewValue: MediaPreviewValue? = null
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.success(null) },
setMediaPreviewValueResult = { value ->
setMediaPreviewValue = value
Result.success(Unit)
}
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify server was updated with local value
assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Private)
// Verify local data was cleared
assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull()
}
@Test
fun `when both local values exist and server has no config, migrates both to server`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setHideInviteAvatars(true)
setTimelineMediaPreviewValue(MediaPreviewValue.Off)
}
var setHideInviteAvatarsValue: Boolean? = null
var setMediaPreviewValue: MediaPreviewValue? = null
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.success(null) },
setHideInviteAvatarsResult = { value ->
setHideInviteAvatarsValue = value
Result.success(Unit)
},
setMediaPreviewValueResult = { value ->
setMediaPreviewValue = value
Result.success(Unit)
}
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify server was updated with both local values
assertThat(setHideInviteAvatarsValue).isTrue()
assertThat(setMediaPreviewValue).isEqualTo(MediaPreviewValue.Off)
// Verify local data was cleared
assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isNull()
assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isNull()
}
@Test
fun `when fetch config fails, migration does nothing`() = runTest {
val appPreferencesStore = InMemoryAppPreferencesStore().apply {
setHideInviteAvatars(true)
setTimelineMediaPreviewValue(MediaPreviewValue.Private)
}
val mediaPreviewService = FakeMediaPreviewService(
fetchMediaPreviewConfigResult = { Result.failure(Exception("Network error")) }
)
val migration = createMigration(appPreferencesStore, mediaPreviewService)
migration().join()
// Verify local data was not cleared since migration failed
assertThat(appPreferencesStore.getHideInviteAvatarsFlow().first()).isTrue()
assertThat(appPreferencesStore.getTimelineMediaPreviewValueFlow().first()).isEqualTo(MediaPreviewValue.Private)
}
private fun TestScope.createMigration(
appPreferencesStore: InMemoryAppPreferencesStore,
mediaPreviewService: FakeMediaPreviewService
) = MediaPreviewConfigMigration(
mediaPreviewService = mediaPreviewService,
appPreferencesStore = appPreferencesStore,
sessionCoroutineScope = this
)
}

View file

@ -98,6 +98,10 @@ allprojects {
// Uncomment to suppress Compose Kotlin compiler compatibility warning
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
// Fix compilation warning for annotations
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
freeCompilerArgs.add("-Xannotation-default-target=first-only")
}
}
}

@ -1 +1 @@
Subproject commit 4a07c862a23a9fd1418eabf132cf9d6b25ea4927
Subproject commit b7ababb9537da8bec254b8ed00b5a4122e9f3e3b

View file

@ -0,0 +1,2 @@
Main changes in this version: improve accessibility.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Del anonyme brugsdata for at hjælpe os med at identificere problemer."</string>
<string name="screen_analytics_settings_read_terms">"Du kan læse alle vores vilkår %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"her"</string>
<string name="screen_analytics_settings_share_data">"Del analysedata"</string>
</resources>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"داده های استفاده ناشناس را به اشتراک بگذارید تا به ما در شناسایی مشکلات کمک کند."</string>
<string name="screen_analytics_settings_read_terms">"شما می‌توانید تمام شرایط ما را بخوانید%1$s ."</string>
<string name="screen_analytics_settings_read_terms_content_link">"این‌جا"</string>
<string name="screen_analytics_settings_share_data">"هم رسانی داده‌های تحلیلی"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Vi vil ikke registrere eller profilere nogen personlige data"</string>
<string name="screen_analytics_prompt_help_us_improve">"Del anonyme brugsdata for at hjælpe os med at identificere problemer."</string>
<string name="screen_analytics_prompt_read_terms">"Du kan læse alle vores vilkår %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"her"</string>
<string name="screen_analytics_prompt_settings">"Du kan slå dette fra når som helst"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Vi deler ikke dine data med tredjeparter"</string>
<string name="screen_analytics_prompt_title">"Hjælp med at forbedre %1$s"</string>
</resources>

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"ما هیچ گونه اطلاعات شخصی را ضبط یا نمایه‌سازی نمی‌کنیم"</string>
<string name="screen_analytics_prompt_help_us_improve">"داده های استفاده ناشناس را به اشتراک بگذارید تا به ما در شناسایی مشکلات کمک کند."</string>
<string name="screen_analytics_prompt_read_terms">"شما می‌توانید تمام شرایط ما را بخوانید%1$s ."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"این‌جا"</string>
<string name="screen_analytics_prompt_settings">"می‌توانید در هر زمان خاموشش کنید"</string>
<string name="screen_analytics_prompt_third_party_sharing">"داده‌هایتان را با سوم‌شخص‌ها هم‌نمی‌رسانیم"</string>

View file

@ -30,6 +30,9 @@ data class WidgetMessage(
@Serializable
enum class Action {
@SerialName("io.element.join")
Join,
@SerialName("im.vector.hangup")
HangUp,

View file

@ -7,16 +7,6 @@
package io.element.android.features.call.impl.pip
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class PictureInPictureStateProvider : PreviewParameterProvider<PictureInPictureState> {
override val values: Sequence<PictureInPictureState>
get() = sequenceOf(
aPictureInPictureState(supportPip = true),
aPictureInPictureState(supportPip = true, isInPictureInPicture = true),
)
}
fun aPictureInPictureState(
supportPip: Boolean = false,
isInPictureInPicture: Boolean = false,

View file

@ -48,9 +48,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
@ -91,6 +88,7 @@ class CallScreenPresenter @AssistedInject constructor(
var webViewError by remember { mutableStateOf<String?>(null) }
val languageTag = languageTagProvider.provideLanguageTag()
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
DisposableEffect(Unit) {
coroutineScope.launch {
// Sets the call as joined
@ -145,17 +143,25 @@ class CallScreenPresenter @AssistedInject constructor(
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
if (parsedMessage.action == WidgetMessage.Action.Close) {
close(callWidgetDriver.value, navigator)
} else if (parsedMessage.action == WidgetMessage.Action.SendEvent) {
// This event is received when a member joins the call, the first one will be the current one
val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull
if (type == "org.matrix.msc3401.call.member") {
isJoinedCall = true
}
} else if (parsedMessage.action == WidgetMessage.Action.Join) {
isJoinedCall = true
}
}
}
.launchIn(this)
}
LaunchedEffect(Unit) {
// Wait for the call to be joined, if it takes too long, we display an error
delay(10.seconds)
if (!isJoinedCall) {
Timber.w("The call took too long to be joined. Displaying an error before exiting.")
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
webViewError = ""
}
}
}
fun handleEvents(event: CallScreenEvents) {

View file

@ -11,6 +11,7 @@ import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
@ -19,7 +20,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -32,11 +32,9 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.impl.R
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason
import io.element.android.features.call.impl.utils.WebViewAudioManager
@ -44,13 +42,11 @@ import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
@ -60,7 +56,6 @@ interface CallScreenNavigator {
fun close()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun CallScreenView(
state: CallScreenState,
@ -78,19 +73,6 @@ internal fun CallScreenView(
Scaffold(
modifier = modifier,
topBar = {
if (!pipState.isInPictureInPicture) {
TopAppBar(
title = { Text(stringResource(R.string.element_call)) },
navigationIcon = {
BackButton(
imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(),
onClick = ::handleBack,
)
}
)
}
}
) { padding ->
BackHandler {
handleBack()
@ -127,9 +109,11 @@ internal fun CallScreenView(
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onCreateWebView = { webView ->
webView.addBackHandler(onBackPressed = ::handleBack)
val interceptor = WebViewWidgetMessageInterceptor(
webView = webView,
onUrlLoaded = { url ->
webView.evaluateJavascript("controls.onBackButtonPressed = () => { backHandler.onBackPressed() }", null)
if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode")
webViewAudioManager?.onCallStarted()
@ -282,6 +266,17 @@ private fun WebView.setup(
}
}
private fun WebView.addBackHandler(onBackPressed: () -> Unit) {
addJavascriptInterface(
object {
@Suppress("unused")
@JavascriptInterface
fun onBackPressed() = onBackPressed()
},
"backHandler"
)
}
@PreviewsDayNight
@Composable
internal fun CallScreenViewPreview(
@ -294,18 +289,6 @@ internal fun CallScreenViewPreview(
)
}
@PreviewsDayNight
@Composable
internal fun CallScreenPipViewPreview(
@PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState,
) = ElementPreview {
CallScreenView(
state = aCallScreenState(),
pipState = state,
requestPermissions = { _, _ -> },
)
}
@PreviewsDayNight
@Composable
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@ -74,7 +75,8 @@ internal fun IncomingCallScreen(
name = notificationData.senderName,
url = notificationData.avatarUrl,
size = AvatarSize.IncomingCall,
)
),
avatarType = AvatarType.User,
)
Spacer(modifier = Modifier.height(24.dp))
Text(

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Probíhající hovor"</string>
<string name="call_foreground_service_message_android">"Klepněte pro návrat k hovoru"</string>
<string name="call_foreground_service_title_android">"☎️ Probíhá hovor"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call nepodporuje používání Bluetooth zvukových zařízení v této verzi systému Android. Vyberte jiné zvukové zařízení."</string>
<string name="screen_incoming_call_subtitle_android">"Příchozí Element Call"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Galwad cyfredol"</string>
<string name="call_foreground_service_message_android">"Tapio i ddychwelyd i\'r alwad"</string>
<string name="call_foreground_service_title_android">"☎️ Galwad ar y gweill"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Nid yw Element Call yn cefnogi defnyddio dyfeisiau sain Bluetooth yn y fersiwn Android hon. Dewiswch ddyfais sain wahanol."</string>
<string name="screen_incoming_call_subtitle_android">"Galwad Element"</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Igangværende opkald"</string>
<string name="call_foreground_service_message_android">"Tryk for at vende tilbage til opkaldet"</string>
<string name="call_foreground_service_title_android">"☎️ Opkald i gang"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call understøtter desværre ikke brug af Bluetooth-lydenheder i denne Android-version. Vælg venligst en anden lydenhed."</string>
<string name="screen_incoming_call_subtitle_android">"Indgående Element opkald"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Laufender Anruf"</string>
<string name="call_foreground_service_message_android">"Tippen, um zum Anruf zurückzukehren"</string>
<string name="call_foreground_service_title_android">"☎️ Anruf läuft"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"In dieser Android-Version unterstützt Element Call derzeit keine Bluetooth-Audiogeräte. Bitte wählen Sie ein anderes Audiogerät aus."</string>
<string name="screen_incoming_call_subtitle_android">"Eingehender Element Call"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Συνεχής κλήση"</string>
<string name="call_foreground_service_message_android">"Πάτα για να επιστρέψεις στην κλήση"</string>
<string name="call_foreground_service_title_android">"☎️ Κλήση σε εξέλιξη"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Το Element Call δεν υποστηρίζει τη χρήση συσκευών ήχου Bluetooth σε αυτήν την έκδοση Android. Επέλεξε μια διαφορετική συσκευή ήχου."</string>
<string name="screen_incoming_call_subtitle_android">"Εισερχόμενη κλήση Element"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Käimasolev kõne"</string>
<string name="call_foreground_service_message_android">"Kõne juurde naasmiseks klõpsa"</string>
<string name="call_foreground_service_title_android">"☎️ Kõne on pooleli"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade."</string>
<string name="screen_incoming_call_subtitle_android">"Sissetulev Element Calli kõne"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Folyamatban lévő hívás"</string>
<string name="call_foreground_service_message_android">"Koppintson a híváshoz való visszatéréshez"</string>
<string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Az Element Call nem támogatja a Bluetooth hangeszközök használatát ebben az Android-verzióban. Válasszon másik hangeszközt."</string>
<string name="screen_incoming_call_subtitle_android">"Bejövő Element hívás"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Panggilan berlangsung"</string>
<string name="call_foreground_service_message_android">"Ketuk untuk kembali ke panggilan"</string>
<string name="call_foreground_service_title_android">"☎️ Panggilan sedang berlangsung"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call tidak mendukung penggunaan perangkat audio Bluetooth di versi Android ini. Silakan pilih perangkat audio yang berbeda."</string>
<string name="screen_incoming_call_subtitle_android">"Element Call Masuk"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Chiamata in corso"</string>
<string name="call_foreground_service_message_android">"Tocca per tornare alla chiamata"</string>
<string name="call_foreground_service_title_android">"☎️ Chiamata in corso"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call non supporta l\'uso di dispositivi audio Bluetooth in questa versione di Android. Seleziona un dispositivo audio diverso."</string>
<string name="screen_incoming_call_subtitle_android">"Chiamata Element Call in arrivo"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Chamada em andamento"</string>
<string name="call_foreground_service_message_android">"Toque para retornar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em andamento"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"O Element Call não tem suporte a dispositivos de áudio Bluetooth nesta versão do Android. Por favor, selecione um dispositivo de áudio diferente."</string>
<string name="screen_incoming_call_subtitle_android">"Chamada do Element recebida"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"As chamadas do Element não permitem o uso de dispositivos de áudio Bluetooth nesta versão do Android. Por favor, seleciona outro dispositivo."</string>
<string name="screen_incoming_call_subtitle_android">"A receber chamada da Element"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Prebiehajúci hovor"</string>
<string name="call_foreground_service_message_android">"Ťuknutím sa vrátite k hovoru"</string>
<string name="call_foreground_service_title_android">"☎️ Prebieha hovor"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call nepodporuje používanie zvukových zariadení Bluetooth v tejto verzii systému Android. Vyberte iné zvukové zariadenie."</string>
<string name="screen_incoming_call_subtitle_android">"Prichádzajúci hovor Element Call"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Поточний виклик"</string>
<string name="call_foreground_service_message_android">"Торкніться, щоб повернутися до виклику"</string>
<string name="call_foreground_service_title_android">"☎️ Триває виклик"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call не підтримує використання аудіопристроїв Bluetooth у цій версії Android. Виберіть інший аудіопристрій."</string>
<string name="screen_incoming_call_subtitle_android">"Вхідний виклик Element"</string>
</resources>

View file

@ -1,83 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CallScreenViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back when pip is not supported hangs up`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
aCallScreenState(
eventSink = eventsRecorder
),
aPictureInPictureState(
supportPip = false,
eventSink = pipEventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSize(2)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
pipEventsRecorder.assertSize(1)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
}
@Test
fun `clicking on back when pip is supported enables PiP`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
aCallScreenState(
eventSink = eventsRecorder
),
aPictureInPictureState(
supportPip = true,
eventSink = pipEventsRecorder,
),
)
rule.pressBack()
eventsRecorder.assertSize(1)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
pipEventsRecorder.assertSize(2)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
pipEventsRecorder.assertTrue(1) { it == PictureInPictureEvents.EnterPictureInPicture }
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCallScreenView(
state: CallScreenState,
pipState: PictureInPictureState,
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit = { _, _ -> },
) {
setContent {
CallScreenView(
state = state,
pipState = pipState,
requestPermissions = requestPermissions,
)
}
}

View file

@ -225,7 +225,7 @@ import kotlin.time.Duration.Companion.seconds
}
@Test
fun `present - a received room member message makes the call to be active`() = runTest {
fun `present - a received 'joined' action makes the call to be active`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
@ -248,13 +248,10 @@ import kotlin.time.Duration.Companion.seconds
messageInterceptor.givenInterceptedMessage(
"""
{
"action":"send_event",
"action":"io.element.join",
"api":"fromWidget",
"widgetId":"1",
"requestId":"1",
"data":{
"type":"org.matrix.msc3401.call.member"
}
"requestId":"1"
}
""".trimIndent()
)
@ -264,6 +261,40 @@ import kotlin.time.Duration.Companion.seconds
}
}
@Test
fun `present - if in room mode and no join action is received an error is displayed`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
navigator = navigator,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
screenTracker = FakeScreenTracker {},
)
val messageInterceptor = FakeWidgetMessageInterceptor()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.isCallActive).isFalse()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
skipItems(2)
// Wait for the timeout to trigger
advanceTimeBy(10.seconds)
val finalState = awaitItem()
assertThat(finalState.isCallActive).isFalse()
// The error dialog that will force the user to leave the call is displayed
assertThat(finalState.webViewError).isNotNull()
assertThat(finalState.webViewError).isEmpty()
}
}
@Test
fun `present - automatically sets the isInCall state when starting the call and disposing the screen`() = runTest {
val navigator = FakeCallScreenNavigator()

View file

@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.UserListView
import io.element.android.features.createroom.impl.userlist.UserListEvents
@ -23,9 +22,7 @@ import io.element.android.features.createroom.impl.userlist.UserListState
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@ -74,12 +71,7 @@ private fun AddPeopleViewTopBar(
onNextClick: () -> Unit,
) {
TopAppBar(
title = {
Text(
text = stringResource(id = R.string.screen_create_room_add_people_title),
style = ElementTheme.typography.aliasScreenTitle
)
},
titleStr = stringResource(id = R.string.screen_create_room_add_people_title),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip

View file

@ -40,13 +40,14 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
@ -187,12 +188,7 @@ private fun ConfigureRoomToolbar(
onNextClick: () -> Unit,
) {
TopAppBar(
title = {
Text(
text = stringResource(R.string.screen_create_room_title),
style = ElementTheme.typography.aliasScreenTitle,
)
},
titleStr = stringResource(R.string.screen_create_room_title),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
TextButton(
@ -219,6 +215,8 @@ private fun RoomNameWithAvatar(
) {
UnsavedAvatar(
avatarUri = avatarUri,
avatarSize = AvatarSize.EditRoomDetails,
avatarType = AvatarType.Room(),
modifier = Modifier.clickable(onClick = onAvatarClick),
)

View file

@ -36,7 +36,6 @@ import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -137,12 +136,7 @@ private fun CreateRoomRootViewTopBar(
onCloseClick: () -> Unit,
) {
TopAppBar(
title = {
Text(
text = stringResource(id = CommonStrings.action_start_chat),
style = ElementTheme.typography.aliasScreenTitle,
)
},
titleStr = stringResource(id = CommonStrings.action_start_chat),
navigationIcon = {
BackButton(
imageVector = CompoundIcons.Close(),

View file

@ -3,10 +3,21 @@
<string name="screen_create_room_action_create_room">"Нова стая"</string>
<string name="screen_create_room_add_people_title">"Поканване на хора"</string>
<string name="screen_create_room_error_creating_room">"Възникна грешка при създаването на стаята"</string>
<string name="screen_create_room_private_option_description">"Съобщенията в тази стая са шифровани. Шифроването не може да бъде изключено впоследствие."</string>
<string name="screen_create_room_private_option_title">"Частна стая (само с покана)"</string>
<string name="screen_create_room_public_option_description">"Съобщенията не са шифровани и всеки може да ги прочете. Можете да активирате шифроването на по-късна дата."</string>
<string name="screen_create_room_private_option_description">"Само поканени хора имат достъп до тази стая. Всички съобщения са шифровани от край до край."</string>
<string name="screen_create_room_private_option_title">"Частна стая"</string>
<string name="screen_create_room_public_option_description">"Всеки може да намери тази стая.
Можете да промените това по всяко време в настройките на стаята."</string>
<string name="screen_create_room_public_option_title">"Общодостъпна стая"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Всеки може да се присъедини към тази стая"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Всеки"</string>
<string name="screen_create_room_room_address_section_footer">"За да бъде тази стая видима в директорията на общодостъпните стаи, ще ви е необходим адрес на стаята."</string>
<string name="screen_create_room_room_name_label">"Име на стаята"</string>
<string name="screen_create_room_room_visibility_section_title">"Видимост на стаята"</string>
<string name="screen_create_room_title">"Създаване на стая"</string>
<string name="screen_create_room_topic_label">"Тема за разговор (незадължително)"</string>
<string name="screen_start_chat_join_room_by_address_action">"Присъединяване към стая по адрес"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Не е валиден адрес"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Въведете…"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Стаята не е намерена"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"напр. #room-name:matrix.org"</string>
</resources>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Nyt rum"</string>
<string name="screen_create_room_add_people_title">"Invitér folk"</string>
<string name="screen_create_room_error_creating_room">"Der opstod en fejl ved oprettelsen af rummet"</string>
<string name="screen_create_room_private_option_description">"Kun inviterede personer kan få adgang til dette rum. Alle meddelelser er ende-til-ende krypteret."</string>
<string name="screen_create_room_private_option_title">"Privat rum"</string>
<string name="screen_create_room_public_option_description">"Alle kan finde dette rum.
Du kan ændre dette når som helst i rummets indstillinger."</string>
<string name="screen_create_room_public_option_title">"Offentligt rum"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Alle kan deltage i dette rum"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Enhver"</string>
<string name="screen_create_room_room_access_section_header">"Adgang til rummet"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Alle kan bede om at deltage i rummet, men en administrator eller en moderator skal acceptere anmodningen"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Spørg om at deltage"</string>
<string name="screen_create_room_room_address_section_footer">"Hvis dette rum skal være synligt i det offentlige register, skal du bruge en rum-adresse."</string>
<string name="screen_create_room_room_address_section_title">"Rummets adresse"</string>
<string name="screen_create_room_room_name_label">"Navn på rum"</string>
<string name="screen_create_room_room_visibility_section_title">"Rummets synlighed"</string>
<string name="screen_create_room_title">"Opret et rum"</string>
<string name="screen_create_room_topic_label">"Emne (valgfrit)"</string>
<string name="screen_room_directory_search_title">"Register over rum"</string>
<string name="screen_start_chat_error_starting_chat">"Der opstod en fejl under forsøget på at starte en samtale"</string>
<string name="screen_start_chat_join_room_by_address_action">"Tilslut dig rummet med adressen"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ikke en gyldig adresse"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Indtast…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Matchende rum fundet"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Rum ikke fundet"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"f.eks. #rummets-navn:matrix.org"</string>
</resources>

View file

@ -1,30 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Νέο δωμάτιο"</string>
<string name="screen_create_room_action_create_room">"Νέα αίθουσα"</string>
<string name="screen_create_room_add_people_title">"Πρόσκληση ατόμων"</string>
<string name="screen_create_room_error_creating_room">αρουσιάστηκε σφάλμα κατά τη δημιουργία του δωματίου"</string>
<string name="screen_create_room_private_option_description">"Μόνο άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτό το δωμάτιο. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο."</string>
<string name="screen_create_room_private_option_title">"Ιδιωτικό δωμάτιο"</string>
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτό το δωμάτιο.
Μπορείς να το αλλάξεις ανά πάσα στιγμή στις ρυθμίσεις δωματίου."</string>
<string name="screen_create_room_public_option_title">"Δημόσιο δωμάτιο"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτό το δωμάτιο"</string>
<string name="screen_create_room_error_creating_room">ροέκυψε σφάλμα κατά τη δημιουργία της αίθουσας"</string>
<string name="screen_create_room_private_option_description">"Μόνο τα άτομα που έχουν προσκληθεί μπορούν να έχουν πρόσβαση σε αυτή την αίθουσα. Όλα τα μηνύματα είναι κρυπτογραφημένα από άκρο σε άκρο."</string>
<string name="screen_create_room_private_option_title">"Ιδιωτική αίθουσα"</string>
<string name="screen_create_room_public_option_description">"Ο καθένας μπορεί να βρει αυτή την αίθουσα.
Αυτό μπορείτε να το αλλάξετε ανά πάσα στιγμή στις ρυθμίσεις της αίθουσας."</string>
<string name="screen_create_room_public_option_title">"Δημόσια αίθουσα"</string>
<string name="screen_create_room_room_access_section_anyone_option_description">"Οποιοσδήποτε μπορεί να συμμετάσχει σε αυτή την αίθουσα"</string>
<string name="screen_create_room_room_access_section_anyone_option_title">"Οποιοσδήποτε"</string>
<string name="screen_create_room_room_access_section_header">"Πρόσβαση Δωματίου"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά ένας διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα"</string>
<string name="screen_create_room_room_access_section_header">"Πρόσβαση στην Αίθουσα"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στην αίθουσα, αλλά ένας διαχειριστής ή ένας συντονιστής θα πρέπει να αποδεχτεί το αίτημα"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Αίτημα συμμετοχής"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατό αυτό το δωμάτιο στον κατάλογο των δημόσιων δωματίων, θα χρειαστείς μια διεύθυνση δωματίου."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση δωματίου"</string>
<string name="screen_create_room_room_name_label">"Όνομα δωματίου"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα δωματίου"</string>
<string name="screen_create_room_title">"Δημιούργησε ένα δωμάτιο"</string>
<string name="screen_create_room_room_address_section_footer">"Για να είναι ορατή αυτή η αίθουσα στον δημόσιο κατάλογο αιθουσών, θα χρειαστείτε μια διεύθυνση αίθουσας."</string>
<string name="screen_create_room_room_address_section_title">"Διεύθυνση αίθουσας"</string>
<string name="screen_create_room_room_name_label">"Όνομα αίθουσας"</string>
<string name="screen_create_room_room_visibility_section_title">"Ορατότητα αίθουσας"</string>
<string name="screen_create_room_title">"Δημιουργία αίθουσας"</string>
<string name="screen_create_room_topic_label">"Θέμα (προαιρετικό)"</string>
<string name="screen_room_directory_search_title">"Κατάλογος δωματίων"</string>
<string name="screen_room_directory_search_title">"Κατάλογος αιθουσών"</string>
<string name="screen_start_chat_error_starting_chat">"Παρουσιάστηκε σφάλμα κατά την προσπάθεια έναρξης μιας συνομιλίας"</string>
<string name="screen_start_chat_join_room_by_address_action">"Συμμετοχή σε δωμάτιο μέσω διεύθυνσης"</string>
<string name="screen_start_chat_join_room_by_address_action">"Συμμετοχή σε αίθουσα μέσω διεύθυνσης"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Μη έγκυρη διεύθυνση"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Εισάγετε…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Βρέθηκε το αντίστοιχο δωμάτιο"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Το δωμάτιο δε βρέθηκε"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"π.χ. #όνομα-δωματίου:matrix.org"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Βρέθηκε η αντίστοιχη αίθουσα"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Η αίθουσα δεν βρέθηκε"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"π.χ. #όνομα-αίθουσας:matrix.org"</string>
</resources>

View file

@ -18,6 +18,7 @@
<string name="screen_create_room_title">"ایجاد اتاق"</string>
<string name="screen_create_room_topic_label">"موضوع (اختیاری)"</string>
<string name="screen_room_directory_search_title">"فهرست اتاق‌ها"</string>
<string name="screen_start_chat_error_starting_chat">"هنگام تلاش برای شروع چت خطایی روی داد"</string>
<string name="screen_start_chat_join_room_by_address_action">"پیوستن به اتاق با نشانی"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"نشانی معتبری نیست"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"ورود…"</string>

View file

@ -14,10 +14,17 @@ Anda dapat mengubah ini kapan pun dalam pengaturan ruangan."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Siapa pun dapat meminta untuk bergabung dengan ruangan tetapi administrator atau moderator harus menerima permintaan tersebut"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Minta untuk bergabung"</string>
<string name="screen_create_room_room_address_section_footer">"Supaya ruangan ini terlihat di direktori ruangan publik, Anda memerlukan alamat ruangan."</string>
<string name="screen_create_room_room_address_section_title">"Alamat ruangan"</string>
<string name="screen_create_room_room_name_label">"Nama ruangan"</string>
<string name="screen_create_room_room_visibility_section_title">"Keterlihatan ruangan"</string>
<string name="screen_create_room_title">"Buat ruangan"</string>
<string name="screen_create_room_topic_label">"Topik (opsional)"</string>
<string name="screen_room_directory_search_title">"Direktori ruangan"</string>
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
<string name="screen_start_chat_join_room_by_address_action">"Bergabung dalam ruangan berdasarkan alamat"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Bukan alamat yang valid"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Masuk…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Ruangan yang cocok ditemukan"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Ruangan tidak ditemukan"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"mis. #nama-ruangan:matrix.org"</string>
</resources>

View file

@ -57,7 +57,6 @@ import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusN
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -85,12 +84,7 @@ fun AccountDeactivationView(
navigationIcon = {
BackButton(onClick = onBackClick)
},
title = {
Text(
text = stringResource(R.string.screen_deactivate_account_title),
style = ElementTheme.typography.aliasScreenTitle,
)
},
titleStr = stringResource(R.string.screen_deactivate_account_title),
)
},
) { padding ->

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Моля, потвърдете, че искате да деактивирате акаунта си. Това действие не може да бъде отменено."</string>
<string name="screen_deactivate_account_title">"Деактивиране на акаунта"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"Bekræft venligst, at du vil deaktivere din konto. Denne handling kan ikke fortrydes."</string>
<string name="screen_deactivate_account_delete_all_messages">"Slet alle mine beskeder"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Advarsel: Fremtidige brugere kan muligvis se ufuldstændige samtaler."</string>
<string name="screen_deactivate_account_description">"Deaktivering af din konto er %1$s, det vil:"</string>
<string name="screen_deactivate_account_description_bold_part">"irreversibel"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s din konto (du kan ikke logge ind igen, og dit ID kan ikke genbruges)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Deaktiver permanent"</string>
<string name="screen_deactivate_account_list_item_2">"Fjern dig fra alle samtalerum"</string>
<string name="screen_deactivate_account_list_item_3">"Slette dine kontooplysninger fra vores identitetsserver."</string>
<string name="screen_deactivate_account_list_item_4">"Dine beskeder vil stadig være synlige for registrerede brugere, men vil ikke være tilgængelige for nye eller uregistrerede brugere, hvis du vælger at slette dem."</string>
<string name="screen_deactivate_account_title">"Deaktiver konto"</string>
</resources>

View file

@ -7,7 +7,7 @@
<string name="screen_deactivate_account_description_bold_part">"μη αναστρέψιμο"</string>
<string name="screen_deactivate_account_list_item_1">"%1$s τον λογαριασμό σου (δεν μπορείς να συνδεθείς ξανά και το αναγνωριστικό σου δεν μπορεί να επαναχρησιμοποιηθεί)."</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"Μόνιμη απενεργοποίηση"</string>
<string name="screen_deactivate_account_list_item_2">"Σε αφαιρέσει από όλα τα δωμάτια συνομιλίας."</string>
<string name="screen_deactivate_account_list_item_2">"Αποχώρησή σας από όλες τις αίθουσες συνομιλίας."</string>
<string name="screen_deactivate_account_list_item_3">"Διαγράψει τα στοιχεία του λογαριασμού σου από τον διακομιστή ταυτότητάς μας."</string>
<string name="screen_deactivate_account_list_item_4">"Τα μηνύματά σου θα εξακολουθούν να είναι ορατά στους εγγεγραμμένους χρήστες, αλλά δεν θα είναι διαθέσιμα σε νέους ή μη εγγεγραμμένους χρήστες εάν επιλέξεις να τα διαγράψεις."</string>
<string name="screen_deactivate_account_title">"Απενεργοποίηση λογαριασμού"</string>

View file

@ -16,8 +16,6 @@ interface EnterpriseService {
fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
suspend fun isElementCallAvailable(): Boolean
fun semanticColorsLight(): SemanticColors
fun semanticColorsDark(): SemanticColors

View file

@ -0,0 +1,12 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.api
interface SessionEnterpriseService {
suspend fun isElementCallAvailable(): Boolean
}

View file

@ -25,8 +25,6 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementCallAvailable(): Boolean = true
override fun semanticColorsLight(): SemanticColors = compoundColorsLight
override fun semanticColorsDark(): SemanticColors = compoundColorsDark

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.libraries.di.SessionScope
import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService {
override suspend fun isElementCallAvailable(): Boolean = true
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.impl
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultSessionEnterpriseServiceTest {
@Test
fun `isElementCallAvailable is always true`() = runTest {
val service = DefaultSessionEnterpriseService()
assertThat(service.isElementCallAvailable()).isTrue()
}
}

View file

@ -18,7 +18,6 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
@ -36,10 +35,6 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult()
}
override fun semanticColorsLight(): SemanticColors {
return semanticColorsLightResult()
}

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.enterprise.test
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeSessionEnterpriseService(
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
) : SessionEnterpriseService {
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult()
}
}

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -149,6 +150,7 @@ private fun NotificationRow(
) {
Avatar(
avatarData = AvatarData(id = avatarColorsId, name = avatarLetter, size = AvatarSize.NotificationsOptIn),
avatarType = AvatarType.User,
)
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Box(

View file

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете да потвърдите?"</string>
<string name="screen_identity_confirmation_subtitle">"Потвърдете това устройство, за да настроите защитени съобщения."</string>
<string name="screen_identity_confirmation_title">"Потвърдете самоличността си"</string>
<string name="screen_identity_confirmation_use_another_device">"Използване на друго устройство"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Използване на ключ за възстановяване"</string>
<string name="screen_identity_confirmed_title">"Устройството е потвърдено"</string>
<string name="screen_identity_use_another_device">"Използване на друго устройство"</string>
<string name="screen_notification_optin_subtitle">"Можете да промените настройките си по-късно."</string>
<string name="screen_notification_optin_title">"Разрешете известията и никога не пропускайте съобщение"</string>
<string name="screen_session_verification_enter_recovery_key">"Въвеждане на ключ за възстановяване"</string>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan ikke bekræfte?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Opret en ny gendannelsesnøgle"</string>
<string name="screen_identity_confirmation_subtitle">"Verificér denne enhed for at konfigurere sikre meddelelser."</string>
<string name="screen_identity_confirmation_title">"Bekræft din identitet"</string>
<string name="screen_identity_confirmation_use_another_device">"Brug en anden enhed"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Brug gendannelsesnøgle"</string>
<string name="screen_identity_confirmed_subtitle">"Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed."</string>
<string name="screen_identity_confirmed_title">"Enhed verificeret"</string>
<string name="screen_identity_use_another_device">"Brug en anden enhed"</string>
<string name="screen_identity_waiting_on_other_device">"Venter på en anden enhed…"</string>
<string name="screen_notification_optin_subtitle">"Du kan ændre dine indstillinger senere."</string>
<string name="screen_notification_optin_title">"Tillad notifikationer, og gå aldrig glip af en besked"</string>
<string name="screen_session_verification_enter_recovery_key">"Indtast gendannelsesnøgle"</string>
<string name="screen_welcome_bullet_1">"Opkald, afstemninger, søgninger og mere vil blive tilføjet senere på året."</string>
<string name="screen_welcome_bullet_2">"Beskedhistorik for krypterede rum er ikke tilgængelig endnu."</string>
<string name="screen_welcome_bullet_3">"Vi vil meget gerne høre fra dig. Fortæl os din mening via indstillingssiden."</string>
<string name="screen_welcome_button">"Lad os komme i gang!"</string>
<string name="screen_welcome_subtitle">"Her er, hvad du har brug for at vide:"</string>
<string name="screen_welcome_title">"Velkommen til %1$s!"</string>
</resources>

View file

@ -14,7 +14,7 @@
<string name="screen_notification_optin_title">"Επέτρεψε τις ειδοποιήσεις και μην χάσεις ούτε ένα μήνυμα"</string>
<string name="screen_session_verification_enter_recovery_key">"Εισαγωγή κλειδιού ανάκτησης"</string>
<string name="screen_welcome_bullet_1">"Κλήσεις, δημοσκοπήσεις, αναζήτηση και άλλα, θα προστεθούν αργότερα φέτος."</string>
<string name="screen_welcome_bullet_2">"Το ιστορικό μηνυμάτων για κρυπτογραφημένα δωμάτια δεν είναι ακόμα διαθέσιμο."</string>
<string name="screen_welcome_bullet_2">"Το ιστορικό μηνυμάτων για κρυπτογραφημένες αίθουσες δεν είναι ακόμη διαθέσιμο."</string>
<string name="screen_welcome_bullet_3">"Θα θέλαμε να ακούσουμε τη γνώμη σου, πες μας τη γνώμη σου μέσω της σελίδας ρυθμίσεων."</string>
<string name="screen_welcome_button">"Πάμε!"</string>
<string name="screen_welcome_subtitle">"Να τί πρέπει να ξέρεις:"</string>

View file

@ -13,6 +13,9 @@
<string name="screen_notification_optin_subtitle">"می‌توانید بعداً تنظیماتتان را تغییر دهید."</string>
<string name="screen_notification_optin_title">"اجازه به آگاهی‌ها و از دست ندادن پیام‌ها"</string>
<string name="screen_session_verification_enter_recovery_key">"ورود کلید بازیابی"</string>
<string name="screen_welcome_bullet_1">"تماس ها، نظرسنجی، جستجو و موارد دیگر در اواخر امسال اضافه خواهند شد."</string>
<string name="screen_welcome_bullet_2">"سابقه پیام برای اتاق های رمزگذاری شده هنوز دردسترس نیست."</string>
<string name="screen_welcome_bullet_3">"ما دوست داریم از شما بشنویم، نظر خود را از طریق صفحه تنظیمات با ما در میان بگذارید."</string>
<string name="screen_welcome_button">"بزن بریم!"</string>
<string name="screen_welcome_subtitle">"چیزهایی که باید بدانید:"</string>
<string name="screen_welcome_title">"به %1$s خوش آمدید!"</string>

View file

@ -9,7 +9,7 @@ plugins {
}
android {
namespace = "io.element.android.features.roomlist.api"
namespace = "io.element.android.features.home.api"
}
dependencies {

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.api
package io.element.android.features.home.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -13,7 +13,7 @@ import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId
interface RoomListEntryPoint : FeatureEntryPoint {
interface HomeEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder

View file

@ -13,7 +13,7 @@ plugins {
}
android {
namespace = "io.element.android.features.roomlist.impl"
namespace = "io.element.android.features.home.impl"
testOptions {
unitTests {
@ -51,8 +51,10 @@ dependencies {
implementation(projects.features.rageshake.api)
implementation(projects.services.analytics.api)
implementation(libs.androidx.datastore.preferences)
implementation(libs.haze)
implementation(libs.haze.materials)
implementation(projects.features.reportroom.api)
api(projects.features.roomlist.api)
api(projects.features.home.api)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

View file

@ -5,30 +5,30 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl
package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomListEntryPoint @Inject constructor() : RoomListEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomListEntryPoint.NodeBuilder {
class DefaultHomeEntryPoint @Inject constructor() : HomeEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): HomeEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : RoomListEntryPoint.NodeBuilder {
override fun callback(callback: RoomListEntryPoint.Callback): RoomListEntryPoint.NodeBuilder {
return object : HomeEntryPoint.NodeBuilder {
override fun callback(callback: HomeEntryPoint.Callback): HomeEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<RoomListFlowNode>(buildContext, plugins)
return parentNode.createNode<HomeFlowNode>(buildContext, plugins)
}
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
sealed interface HomeEvents {
data class SelectHomeNavigationBarItem(val item: HomeNavigationBarItem) : HomeEvents
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl
package io.element.android.features.home.impl
import android.app.Activity
import android.os.Parcelable
@ -24,14 +24,14 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.logout.api.direct.DirectLogoutView
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
@ -41,17 +41,17 @@ import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
class RoomListFlowNode @AssistedInject constructor(
class HomeFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: RoomListPresenter,
private val presenter: HomePresenter,
private val inviteFriendsUseCase: InviteFriendsUseCase,
private val analyticsService: AnalyticsService,
private val acceptDeclineInviteView: AcceptDeclineInviteView,
private val directLogoutView: DirectLogoutView,
private val reportRoomEntryPoint: ReportRoomEntryPoint,
private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint,
) : BaseFlowNode<RoomListFlowNode.NavTarget>(
) : BaseFlowNode<HomeFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
@ -79,27 +79,27 @@ class RoomListFlowNode @AssistedInject constructor(
}
private fun onRoomClick(roomId: RoomId) {
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomClick(roomId) }
plugins<HomeEntryPoint.Callback>().forEach { it.onRoomClick(roomId) }
}
private fun onOpenSettings() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onSettingsClick() }
plugins<HomeEntryPoint.Callback>().forEach { it.onSettingsClick() }
}
private fun onCreateRoomClick() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onCreateRoomClick() }
plugins<HomeEntryPoint.Callback>().forEach { it.onCreateRoomClick() }
}
private fun onSetUpRecoveryClick() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onSetUpRecoveryClick() }
plugins<HomeEntryPoint.Callback>().forEach { it.onSetUpRecoveryClick() }
}
private fun onSessionConfirmRecoveryKeyClick() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClick() }
plugins<HomeEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClick() }
}
private fun onRoomSettingsClick(roomId: RoomId) {
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClick(roomId) }
plugins<HomeEntryPoint.Callback>().forEach { it.onRoomSettingsClick(roomId) }
}
private fun onReportRoomClick(roomId: RoomId) {
@ -116,7 +116,7 @@ class RoomListFlowNode @AssistedInject constructor(
inviteFriendsUseCase.execute(activity)
}
RoomListMenuAction.ReportBug -> {
plugins<RoomListEntryPoint.Callback>().forEach { it.onReportBugClick() }
plugins<HomeEntryPoint.Callback>().forEach { it.onReportBugClick() }
}
}
}
@ -126,8 +126,8 @@ class RoomListFlowNode @AssistedInject constructor(
val state = presenter.present()
val activity = requireNotNull(LocalActivity.current)
RoomListView(
state = state,
HomeView(
homeState = state,
onRoomClick = this::onRoomClick,
onSettingsClick = this::onOpenSettings,
onCreateRoomClick = this::onCreateRoomClick,
@ -140,7 +140,7 @@ class RoomListFlowNode @AssistedInject constructor(
modifier = modifier,
) {
acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState,
state = state.roomListState.acceptDeclineInviteState,
onAcceptInviteSuccess = this::onRoomClick,
onDeclineInviteSuccess = { },
modifier = Modifier

View file

@ -0,0 +1,38 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import io.element.android.compound.tokens.generated.CompoundIcons
enum class HomeNavigationBarItem(
@StringRes
val labelRes: Int,
) {
Chats(
labelRes = R.string.screen_roomlist_main_space_title
),
Spaces(
// TODO Create a new entry in Localazy
labelRes = R.string.screen_roomlist_main_space_title
);
@Composable
fun icon() = when (this) {
Chats -> CompoundIcons.ChatSolid()
// TODO Spaces -> CompoundIcons.Workspace()
Spaces -> CompoundIcons.Code()
}
companion object {
fun from(index: Int): HomeNavigationBarItem {
return entries.getOrElse(index) { Chats }
}
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import javax.inject.Inject
class HomePresenter @Inject constructor(
private val client: MatrixClient,
private val syncService: SyncService,
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter<RoomListState>,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
) : Presenter<HomeState> {
@Composable
override fun present(): HomeState {
val matrixUser = client.userProfile.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
val roomListState = roomListPresenter.present()
val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
}.collectAsState(initial = false)
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
val currentHomeNavigationBarItem by remember {
derivedStateOf {
HomeNavigationBarItem.from(currentHomeNavigationBarItemOrdinal)
}
}
LaunchedEffect(Unit) {
// Force a refresh of the profile
client.getUserProfile()
}
// Avatar indicator
val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator()
val directLogoutState = logoutPresenter.present()
fun handleEvents(event: HomeEvents) {
when (event) {
is HomeEvents.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
}
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
matrixUser = matrixUser.value,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = ::handleEvents,
)
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
@Immutable
data class HomeState(
val matrixUser: MatrixUser,
val showAvatarIndicator: Boolean,
val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,
val roomListState: RoomListState,
val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,
val isSpaceFeatureEnabled: Boolean,
val eventSink: (HomeEvents) -> Unit,
) {
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
}

View file

@ -0,0 +1,69 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.roomlist.RoomListStateProvider
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
open class HomeStateProvider : PreviewParameterProvider<HomeState> {
override val values: Sequence<HomeState>
get() = sequenceOf(
aHomeState(),
aHomeState(hasNetworkConnection = false),
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aHomeState(
isSpaceFeatureEnabled = true,
roomListState = aRoomListState(
// Add more rooms to see the blur effect under the NavigationBar
contentState = aRoomsContentState(
summaries = generateRoomListRoomSummaryList(),
)
),
),
aHomeState(
isSpaceFeatureEnabled = true,
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
),
) + RoomListStateProvider().values.map {
aHomeState(roomListState = it)
}
}
internal fun aHomeState(
matrixUser: MatrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
showAvatarIndicator: Boolean = false,
hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null,
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
roomListState: RoomListState = aRoomListState(),
canReportBug: Boolean = true,
isSpaceFeatureEnabled: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
eventSink: (HomeEvents) -> Unit = {}
) = HomeState(
matrixUser = matrixUser,
showAvatarIndicator = showAvatarIndicator,
hasNetworkConnection = hasNetworkConnection,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = eventSink,
)

View file

@ -5,10 +5,15 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl
@file:OptIn(ExperimentalHazeMaterialsApi::class)
package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -19,32 +24,48 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.components.RoomListContentView
import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.components.RoomListTopBar
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListContextMenu
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.features.roomlist.impl.components.RoomListContentView
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchView
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
@Composable
fun RoomListView(
state: RoomListState,
fun HomeView(
homeState: HomeState,
onRoomClick: (RoomId) -> Unit,
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
@ -57,12 +78,13 @@ fun RoomListView(
modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
) {
val state: RoomListState = homeState.roomListState
val coroutineScope = rememberCoroutineScope()
val firstThrottler = remember { FirstThrottler(300, coroutineScope) }
ConnectivityIndicatorContainer(
modifier = modifier,
isOnline = state.hasNetworkConnection,
isOnline = homeState.hasNetworkConnection,
) { topPadding ->
Box {
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
@ -85,8 +107,8 @@ fun RoomListView(
LeaveRoomView(state = state.leaveRoomState)
RoomListScaffold(
state = state,
HomeScaffold(
state = homeState,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
@ -114,8 +136,8 @@ fun RoomListView(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomListScaffold(
state: RoomListState,
private fun HomeScaffold(
state: HomeState,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomId) -> Unit,
@ -131,38 +153,106 @@ private fun RoomListScaffold(
val appBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState)
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
val roomListState: RoomListState = state.roomListState
BackHandler(
enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats,
) {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats))
}
val hazeState = rememberHazeState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
RoomListTopBar(
title = stringResource(state.currentHomeNavigationBarItem.labelRes),
matrixUser = state.matrixUser,
showAvatarIndicator = state.showAvatarIndicator,
areSearchResultsDisplayed = state.searchState.isSearchActive,
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
onToggleSearch = { roomListState.eventSink(RoomListEvents.ToggleSearchResults) },
onMenuActionClick = onMenuActionClick,
onOpenSettings = onOpenSettings,
scrollBehavior = scrollBehavior,
displayMenuItems = state.displayActions,
displayFilters = state.displayFilters,
filtersState = state.filtersState,
displayFilters = roomListState.displayFilters && state.currentHomeNavigationBarItem == HomeNavigationBarItem.Chats,
filtersState = roomListState.filtersState,
canReportBug = state.canReportBug,
)
},
bottomBar = {
if (state.isSpaceFeatureEnabled) {
NavigationBar(
containerColor = Color.Transparent,
modifier = Modifier
.hazeEffect(
state = hazeState,
style = HazeMaterials.regular(),
)
) {
HomeNavigationBarItem.entries.forEach { item ->
NavigationBarItem(
selected = state.currentHomeNavigationBarItem == item,
onClick = {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
},
icon = {
Icon(
imageVector = item.icon(),
contentDescription = null
)
},
label = {
Text(stringResource(item.labelRes))
}
)
}
}
}
},
content = { padding ->
RoomListContentView(
contentState = state.contentState,
filtersState = state.filtersState,
hideInvitesAvatars = state.hideInvitesAvatars,
eventSink = state.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
)
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
RoomListContentView(
contentState = roomListState.contentState,
filtersState = roomListState.filtersState,
hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onCreateRoomClick,
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
// and include provided bottom padding
contentBottomPadding = 80.dp + padding.calculateBottomPadding(),
modifier = Modifier
.padding(
top = padding.calculateTopPadding(),
bottom = 0.dp,
start = padding.calculateStartPadding(LocalLayoutDirection.current),
end = padding.calculateEndPadding(LocalLayoutDirection.current),
)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState)
)
}
HomeNavigationBarItem.Spaces -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Spaces are coming soon!",
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
}
},
floatingActionButton = {
if (state.displayActions) {
@ -186,9 +276,9 @@ internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight
@Composable
internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) state: RoomListState) = ElementPreview {
RoomListView(
state = state,
internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: HomeState) = ElementPreview {
HomeView(
homeState = state,
onRoomClick = {},
onSettingsClick = {},
onSetUpRecoveryClick = {},

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier

View file

@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
@ -31,22 +31,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListContentState
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.SecurityBannerState
import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.filters.RoomListFilter
import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.contentType
import io.element.android.features.home.impl.filters.RoomListFilter
import io.element.android.features.home.impl.filters.RoomListFiltersEmptyStateResources
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListContentState
import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.SecurityBannerState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -66,6 +67,7 @@ fun RoomListContentView(
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
onCreateRoomClick: () -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
@ -93,6 +95,7 @@ fun RoomListContentView(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
contentBottomPadding = contentBottomPadding,
)
}
}
@ -164,6 +167,7 @@ private fun RoomsView(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier,
) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@ -179,6 +183,7 @@ private fun RoomsView(
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick,
contentBottomPadding = contentBottomPadding,
modifier = modifier.fillMaxSize(),
)
}
@ -192,6 +197,7 @@ private fun RoomsViewList(
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit,
contentBottomPadding: Dp,
modifier: Modifier = Modifier,
) {
val lazyListState = rememberLazyListState()
@ -210,8 +216,7 @@ private fun RoomsViewList(
LazyColumn(
state = lazyListState,
modifier = modifier,
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
contentPadding = PaddingValues(bottom = 80.dp)
contentPadding = PaddingValues(bottom = contentBottomPadding)
) {
when (state.securityBannerState) {
SecurityBannerState.SetUpRecovery -> {
@ -311,7 +316,12 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
RoomListContentView(
contentState = state,
filtersState = aRoomListFiltersState(
filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }
filterSelectionStates = RoomListFilter.entries.map {
FilterSelectionState(
filter = it,
isSelected = true
)
}
),
hideInvitesAvatars = false,
eventSink = {},
@ -319,5 +329,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {},
onRoomClick = {},
onCreateRoomClick = {},
contentBottomPadding = 0.dp,
)
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
enum class RoomListMenuAction {
InviteFriends,

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -34,20 +34,23 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.element.android.appconfig.RoomListConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersView
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.RoomListFiltersView
import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatarBloom
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -75,6 +78,7 @@ private val avatarBloomSize = 430.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomListTopBar(
title: String,
matrixUser: MatrixUser,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
@ -89,6 +93,7 @@ fun RoomListTopBar(
modifier: Modifier = Modifier,
) {
DefaultRoomListTopBar(
title = title,
matrixUser = matrixUser,
showAvatarIndicator = showAvatarIndicator,
areSearchResultsDisplayed = areSearchResultsDisplayed,
@ -107,6 +112,7 @@ fun RoomListTopBar(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DefaultRoomListTopBar(
title: String,
matrixUser: MatrixUser,
showAvatarIndicator: Boolean,
areSearchResultsDisplayed: Boolean,
@ -193,7 +199,12 @@ private fun DefaultRoomListTopBar(
scrolledContainerColor = Color.Transparent,
),
title = {
Text(text = stringResource(id = R.string.screen_roomlist_main_space_title))
Text(
modifier = Modifier.semantics {
heading()
},
text = title,
)
},
navigationIcon = {
NavigationIcon(
@ -297,6 +308,7 @@ private fun NavigationIcon(
Box {
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
contentDescription = stringResource(CommonStrings.common_settings),
)
if (showAvatarIndicator) {
@ -313,6 +325,7 @@ private fun NavigationIcon(
@Composable
internal fun DefaultRoomListTopBarPreview() = ElementPreview {
DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
showAvatarIndicator = false,
areSearchResultsDisplayed = false,
@ -332,6 +345,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
@Composable
internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
DefaultRoomListTopBar(
title = stringResource(R.string.screen_roomlist_main_space_title),
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
showAvatarIndicator = true,
areSearchResultsDisplayed = false,

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -37,14 +37,15 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@ -172,6 +173,7 @@ private fun RoomSummaryScaffoldRow(
val clickModifier = Modifier.combinedClickable(
onClick = { onClick(room) },
onLongClick = { onLongClick(room) },
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
indication = ripple(),
interactionSource = remember { MutableInteractionSource() }
)
@ -184,11 +186,13 @@ private fun RoomSummaryScaffoldRow(
.padding(horizontal = 16.dp, vertical = 11.dp)
.height(IntrinsicSize.Min),
) {
RoomAvatar(
Avatar(
avatarData = room.avatarData,
heroes = room.heroes,
isTombstoned = room.isTombstoned,
hideAvatarImage = hideAvatarImage,
avatarType = AvatarType.Room(
heroes = room.heroes,
isTombstoned = room.isTombstoned,
),
hideImage = hideAvatarImage,
)
Spacer(modifier = Modifier.width(16.dp))
Column(

View file

@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
package io.element.android.features.home.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview

View file

@ -5,9 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.datasource
package io.element.android.features.home.impl.datasource
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.androidutils.system.DateTimeObserver

View file

@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.datasource
package io.element.android.features.home.impl.datasource
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode

View file

@ -5,21 +5,26 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.di
package io.element.android.features.home.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.features.home.impl.filters.RoomListFiltersPresenter
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.roomlist.RoomListPresenter
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchPresenter
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@Module
interface RoomListModule {
@Binds
fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter<RoomListState>
@Binds
fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter<RoomListSearchState>

View file

@ -5,9 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
/**
* Enum class representing the different filters that can be applied to the room list.

View file

@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import androidx.annotation.StringRes
import io.element.android.features.roomlist.impl.R
import io.element.android.features.home.impl.R
/**
* Holds the resources for the empty state when filters are applied to the room list.

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
sealed interface RoomListFiltersEvents {
data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents

View file

@ -5,12 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionStrategy
import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import kotlinx.collections.immutable.toPersistentList

View file

@ -5,9 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList

View file

@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.toImmutableList
class RoomListFiltersStateProvider : PreviewParameterProvider<RoomListFiltersState> {

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters
package io.element.android.features.home.impl.filters
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@ -147,7 +148,7 @@ private fun RoomListClearFiltersButton(
modifier = Modifier.align(Alignment.Center),
imageVector = CompoundIcons.Close(),
tint = ElementTheme.colors.iconOnSolidPrimary,
contentDescription = stringResource(id = io.element.android.libraries.ui.strings.R.string.action_clear),
contentDescription = stringResource(id = R.string.screen_roomlist_clear_filters),
)
}
}

View file

@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters.selection
package io.element.android.features.home.impl.filters.selection
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomlist.impl.filters.RoomListFilter
import io.element.android.features.home.impl.filters.RoomListFilter
import io.element.android.libraries.di.SessionScope
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject

View file

@ -5,9 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters.selection
package io.element.android.features.home.impl.filters.selection
import io.element.android.features.roomlist.impl.filters.RoomListFilter
import io.element.android.features.home.impl.filters.RoomListFilter
data class FilterSelectionState(
val filter: RoomListFilter,

View file

@ -5,9 +5,9 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.filters.selection
package io.element.android.features.home.impl.filters.selection
import io.element.android.features.roomlist.impl.filters.RoomListFilter
import io.element.android.features.home.impl.filters.RoomListFilter
import kotlinx.coroutines.flow.StateFlow
interface FilterSelectionStrategy {

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.model
package io.element.android.features.home.impl.model
import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.InviteData

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.model
package io.element.android.features.home.impl.model
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.avatar.AvatarData

View file

@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.model
package io.element.android.features.home.impl.model
/**
* Represents the type of display for a room list item.

View file

@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl
package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId

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