Merge branch 'develop' into feature/fga/live_location_rendering

This commit is contained in:
ganfra 2026-04-10 09:50:44 +02:00
commit f7bb5b203e
705 changed files with 6439 additions and 2804 deletions

View file

@ -1,7 +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">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_settings_help_us_improve">"Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas."</string>
<string name="screen_analytics_settings_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"čia"</string>
<string name="screen_analytics_settings_share_data">"Dalytis analitiniais duomenimis"</string>
<string name="screen_analytics_settings_share_data">"Bendrinti analitinius duomenis"</string>
</resources>

View file

@ -1,10 +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">"Mes nekaupsime ir neprofiliuosime jokių asmens duomenų"</string>
<string name="screen_analytics_prompt_help_us_improve">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_prompt_data_usage">"Mes neįrašysime ar neprofiliuosime jokių asmeninių duomenų."</string>
<string name="screen_analytics_prompt_help_us_improve">"Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"čia"</string>
<string name="screen_analytics_prompt_settings">"Tai galite bet kada išjungti"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis"</string>
<string name="screen_analytics_prompt_title">"Padėkite pagerinti %1$s"</string>
<string name="screen_analytics_prompt_settings">"Tai galite išjungti bet kuriuo metu."</string>
<string name="screen_analytics_prompt_third_party_sharing">"Mes nebendrinsime jūsų duomenų su trečiosiomis šalimis."</string>
<string name="screen_analytics_prompt_title">"Padėkite patobulinti „%1$s“"</string>
</resources>

View file

@ -73,7 +73,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixmedia.api)
implementation(projects.libraries.network)
implementation(projects.libraries.preferences.api)

View file

@ -6,9 +6,8 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.test
package io.element.android.features.call.impl.notifications
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId

View file

@ -15,11 +15,11 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.features.call.impl.notifications.aCallNotificationData
import io.element.android.features.call.impl.utils.ActiveCall
import io.element.android.features.call.impl.utils.CallState
import io.element.android.features.call.impl.utils.DefaultActiveCallManager
import io.element.android.features.call.impl.utils.DefaultCurrentCallService
import io.element.android.features.call.test.aCallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId

View file

@ -20,7 +20,6 @@ dependencies {
implementation(projects.libraries.core)
api(projects.features.call.api)
implementation(projects.features.call.impl)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.test)
implementation(projects.tests.testutils)

View file

@ -39,7 +39,6 @@ dependencies {
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.previewutils)
implementation(projects.libraries.usersearch.impl)
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
implementation(projects.libraries.featureflag.api)
@ -52,7 +51,6 @@ dependencies {
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.features.startchat.test)
testImplementation(projects.libraries.featureflag.test)
}

View file

@ -84,7 +84,6 @@ class ConfigureRoomPresenter(
@Composable
override fun present(): ConfigureRoomState {
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState()
val homeserverName = remember { matrixClient.userIdServerName() }
@ -113,12 +112,8 @@ class ConfigureRoomPresenter(
}
var spaces by remember { mutableStateOf<ImmutableList<SpaceRoom>>(persistentListOf()) }
LaunchedEffect(canAddRoomToSpace) {
spaces = if (canAddRoomToSpace) {
matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
} else {
persistentListOf()
}
LaunchedEffect(Unit) {
spaces = matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList()
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
parentSpace?.let {
dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true)

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Nemůžete potvrdit?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Vytvoření nového klíče pro obnovení"</string>
<string name="screen_identity_confirmation_subtitle">"Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv."</string>
<string name="screen_identity_confirmation_title">"Potvrďte, že jste to vy"</string>
<string name="screen_identity_confirmation_subtitle">"Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv."</string>
<string name="screen_identity_confirmation_title">"Potvrďte svou digitální identitu"</string>
<string name="screen_identity_confirmation_use_another_device">"Použít jiné zařízení"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Použít klíč pro obnovení"</string>
<string name="screen_identity_confirmed_subtitle">"Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat."</string>

View file

@ -2,8 +2,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_subtitle">"Vælg, hvordan du vil verificere dig for at konfigurere sikre beskeder."</string>
<string name="screen_identity_confirmation_title">"Bekræft din digitale 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>

View file

@ -94,12 +94,6 @@ class HomePresenter(
}
}
LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
// If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
}
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
currentUserAndNeighbors = currentUserAndNeighbors,

View file

@ -34,5 +34,4 @@ data class HomeState(
) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
}

View file

@ -199,50 +199,41 @@ private fun HomeScaffold(
)
},
floatingActionButton = {
if (state.showNavigationBar) {
val coroutineScope = rememberCoroutineScope()
HomeBottomBar(
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
onItemClick = { item ->
// scroll to top if selecting the same item
if (item == state.currentHomeNavigationBarItem) {
val lazyListStateTarget = when (item) {
HomeNavigationBarItem.Chats -> roomsLazyListState
HomeNavigationBarItem.Spaces -> spacesLazyListState
}
coroutineScope.launch {
if (lazyListStateTarget.firstVisibleItemIndex > 10) {
lazyListStateTarget.scrollToItem(10)
}
// Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls
scrollBehavior.state.heightOffset = 0f
lazyListStateTarget.animateScrollToItem(0)
}
} else {
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
val coroutineScope = rememberCoroutineScope()
HomeBottomBar(
currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
onItemClick = { item ->
// scroll to top if selecting the same item
if (item == state.currentHomeNavigationBarItem) {
val lazyListStateTarget = when (item) {
HomeNavigationBarItem.Chats -> roomsLazyListState
HomeNavigationBarItem.Spaces -> spacesLazyListState
}
},
floatingActionButton = when (state.currentHomeNavigationBarItem) {
coroutineScope.launch {
if (lazyListStateTarget.firstVisibleItemIndex > 10) {
lazyListStateTarget.scrollToItem(10)
}
// Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls
scrollBehavior.state.heightOffset = 0f
lazyListStateTarget.animateScrollToItem(0)
}
} else {
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
}
},
floatingActionButton = {
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
{
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) {
{
HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
}
} else {
// No FAB for spaces if we cannot create spaces
null
HomeNavigationBarItem.Spaces -> {
HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
}
},
)
} else {
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
}
},
)
},
floatingActionButtonPosition = if (state.showNavigationBar) FabPosition.Center else FabPosition.End,
floatingActionButtonPosition = FabPosition.Center,
content = { padding ->
val contentPadding = PaddingValues(
bottom = 96.dp,

View file

@ -17,8 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import kotlinx.collections.immutable.persistentListOf
@ -27,20 +25,15 @@ import kotlinx.coroutines.flow.map
@Inject
class SpaceFiltersPresenter(
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient,
) : Presenter<SpaceFiltersState> {
@Composable
override fun present(): SpaceFiltersState {
val isFeatureEnabled by featureFlagService
.isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters)
.collectAsState(initial = false)
val availableFilters by remember {
matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() }
}.collectAsState(initial = persistentListOf())
if (!isFeatureEnabled || availableFilters.isEmpty()) {
if (availableFilters.isEmpty()) {
return SpaceFiltersState.Disabled
}

View file

@ -15,8 +15,6 @@ import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar
import kotlinx.collections.immutable.persistentListOf
@ -29,11 +27,9 @@ import kotlinx.coroutines.flow.map
class HomeSpacesPresenter(
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
private val featureFlagsService: FeatureFlagService,
) : Presenter<HomeSpacesState> {
@Composable
override fun present(): HomeSpacesState {
val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
val hideInvitesAvatar by client.rememberHideInvitesAvatar()
val spaceRooms by remember {
client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() }
@ -52,7 +48,6 @@ class HomeSpacesPresenter(
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
// TODO enable once we can link to the screen to explore public spaces
canExploreSpaces = false,
eventSink = ::handleEvent,

View file

@ -18,7 +18,6 @@ data class HomeSpacesState(
val spaceRooms: ImmutableList<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canCreateSpaces: Boolean,
val canExploreSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)

View file

@ -30,17 +30,9 @@ open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
),
spaceRooms = aListOfSpaceRooms(),
),
aHomeSpacesState(
space = CurrentSpace.Space(
spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com"))
),
spaceRooms = aListOfSpaceRooms(),
canCreateSpaces = false,
),
aHomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = emptyList(),
canCreateSpaces = true,
),
)
}
@ -50,7 +42,6 @@ internal fun aHomeSpacesState(
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
canCreateSpaces: Boolean = true,
canExploreSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
@ -58,7 +49,6 @@ internal fun aHomeSpacesState(
spaceRooms = spaceRooms.toImmutableList(),
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
canCreateSpaces = canCreateSpaces,
canExploreSpaces = canExploreSpaces,
eventSink = eventSink,
)

View file

@ -55,7 +55,7 @@ fun HomeSpacesView(
onExploreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
if (state.spaceRooms.isEmpty()) {
EmptySpaceHomeView(
modifier = modifier.padding(contentPadding),
onCreateSpaceClick = onCreateSpaceClick,

View file

@ -5,9 +5,9 @@
<string name="banner_battery_optimization_title_android">"Nepřicházejí vám oznámení?"</string>
<string name="banner_new_sound_message">"Váš zvuk oznámení byl aktualizován je jasnější, rychlejší a méně rušivý."</string>
<string name="banner_new_sound_title">"Aktualizovali jsme vaše zvuky"</string>
<string name="banner_set_up_recovery_content">"Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením."</string>
<string name="banner_set_up_recovery_submit">"Nastavení obnovy"</string>
<string name="banner_set_up_recovery_title">"Nastavení obnovy"</string>
<string name="banner_set_up_recovery_content">"Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení."</string>
<string name="banner_set_up_recovery_submit">"Získat klíč pro obnovení"</string>
<string name="banner_set_up_recovery_title">"Zálohujte své chaty"</string>
<string name="confirm_recovery_key_banner_message">"Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Zadejte klíč pro obnovení"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Zapomněli jste klíč pro obnovení?"</string>

View file

@ -5,9 +5,9 @@
<string name="banner_battery_optimization_title_android">"Modtager du ikke notifikationer?"</string>
<string name="banner_new_sound_message">"Dit notifikationsping er blevet opdateret tydeligere, hurtigere og mindre forstyrrende."</string>
<string name="banner_new_sound_title">"Vi har opdateret dine lyde"</string>
<string name="banner_set_up_recovery_content">"Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder."</string>
<string name="banner_set_up_recovery_submit">"Opsæt gendannelse"</string>
<string name="banner_set_up_recovery_title">"Konfigurer gendannelse for at beskytte din konto"</string>
<string name="banner_set_up_recovery_content">"Dine chats sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle."</string>
<string name="banner_set_up_recovery_submit">"Hent gendannelsesnøgle"</string>
<string name="banner_set_up_recovery_title">"Sikkerhedskopier dine samtaler"</string>
<string name="confirm_recovery_key_banner_message">"Bekræft din gendannelsesnøgle for at bevare adgangen til nøglelager og meddelelseshistorik."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Indtast din gendannelsesnøgle"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Har du glemt din gendannelsesnøgle?"</string>

View file

@ -33,7 +33,6 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -79,7 +78,6 @@ class HomePresenterTest {
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
)
assertThat(withUserState.showAvatarIndicator).isFalse()
assertThat(withUserState.showNavigationBar).isTrue()
}
}
@ -158,36 +156,6 @@ class HomePresenterTest {
.with(value(Announcement.Space))
}
}
@Test
fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest {
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
homeSpacesPresenter = homeSpacesPresenter,
announcementService = FakeAnnouncementService(
showAnnouncementResult = {},
)
)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(initialState.showNavigationBar).isTrue()
// User navigate to Spaces
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val spaceState = awaitItem()
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
// The last space is left
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
skipItems(1)
val finalState = awaitItem()
// We are back to Chats
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(finalState.showNavigationBar).isFalse()
}
}
}
internal fun createHomePresenter(

View file

@ -8,8 +8,6 @@
package io.element.android.features.home.impl.spacefilters
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
@ -21,26 +19,9 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class SpaceFiltersPresenterTest {
@Test
fun `present - when feature flag is disabled returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false)
)
)
presenter.test {
val state = awaitItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
}
}
@Test
fun `present - when available filters is empty returns Disabled state`() = runTest {
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
)
)
val presenter = createSpaceFiltersPresenter()
presenter.test {
val state = awaitLastSequentialItem()
assertThat(state).isEqualTo(SpaceFiltersState.Disabled)
@ -48,15 +29,12 @@ class SpaceFiltersPresenterTest {
}
@Test
fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest {
fun `present - when filters exist returns Unselected state`() = runTest {
val spaceFilter = aSpaceServiceFilter(displayName = "Test Space")
val spaceService = FakeSpaceService()
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -75,9 +53,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -99,9 +74,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -129,9 +101,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -159,9 +128,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -196,9 +162,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -224,9 +187,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -271,9 +231,6 @@ class SpaceFiltersPresenterTest {
val matrixClient = FakeMatrixClient(spaceService = spaceService)
val presenter = createSpaceFiltersPresenter(
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true)
),
matrixClient = matrixClient,
)
presenter.test {
@ -302,11 +259,9 @@ class SpaceFiltersPresenterTest {
}
private fun createSpaceFiltersPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
): SpaceFiltersPresenter {
return SpaceFiltersPresenter(
featureFlagService = featureFlagService,
matrixClient = matrixClient,
)
}

View file

@ -11,9 +11,6 @@ package io.element.android.features.home.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.test
@ -26,25 +23,18 @@ class HomeSpacesPresenterTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
// canCreateSpaces is initially false
assertThat(state.canCreateSpaces).isFalse()
assertThat(state.space).isEqualTo(CurrentSpace.Root)
assertThat(state.spaceRooms).isEmpty()
assertThat(state.hideInvitesAvatar).isFalse()
assertThat(state.seenSpaceInvites).isEmpty()
// It'll eventually be true
assertThat(awaitItem().canCreateSpaces).isTrue()
}
}
private fun createPresenter(
client: MatrixClient = FakeMatrixClient(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.CreateSpaces.key to true)),
) = HomeSpacesPresenter(
client = client,
seenInvitesStore = seenInvitesStore,
featureFlagsService = featureFlagsService,
)
}

View file

@ -34,7 +34,7 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.usersearch.impl)
implementation(projects.libraries.usersearch.api)
implementation(libs.coil.compose)
implementation(projects.services.apperror.api)
api(projects.features.invitepeople.api)

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_share_location_live_location_duration_picker_title">"Zvolte, jak dlouho chcete sdílet svou aktuální polohu."</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_share_location_live_location_duration_picker_title">"Vælg, hvor længe du vil dele din aktuelle position."</string>
</resources>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_share_location_live_location_disclaimer_title">"Votre historique de localisation en direct sera enregistré dans le salon et visible par les membres après la fin de la session."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Choisissez la durée pendant laquelle vous partagerez votre position en direct."</string>
</resources>

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_share_location_live_location_disclaimer_title">"Az élő helymeghatározás története a szobában lesz tárolva, és a munkamenet befejezése után is látható marad a tagok számára."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Válassza ki, mennyi ideig szeretné megosztani az aktuális tartózkodási helyét."</string>
</resources>

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_share_location_live_location_disclaimer_title">"실시간 위치 기록은 대화방에 저장되며, 공유 종료 후에도 멤버들이 확인할 수 있습니다."</string>
<string name="screen_share_location_live_location_duration_picker_title">"실시간 위치를 공유할 시간을 선택해 주세요."</string>
</resources>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_share_location_live_location_disclaimer_title">"Your live location history will be stored in the room and visible to members after the session ends."</string>
<string name="screen_share_location_live_location_duration_picker_title">"Choose how long to share your live location."</string>
</resources>

View file

@ -6,6 +6,8 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.location.impl.share
import app.cash.molecule.RecompositionMode
@ -37,6 +39,7 @@ import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule

View file

@ -23,7 +23,7 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Zadejte stejný PIN dvakrát"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN kódy se neshodují."</string>
<string name="screen_app_lock_signout_alert_message">"Abyste mohli pokračovat, budete se muset znovu přihlásit a vytvořit nový PIN"</string>
<string name="screen_app_lock_signout_alert_title">"Jste odhlášeni"</string>
<string name="screen_app_lock_signout_alert_title">"Toto zařízení se odstraňuje"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Máte %1$d pokus pro odemknutí"</item>
<item quantity="few">"Máte %1$d pokusy pro odemknutí"</item>
@ -36,5 +36,5 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a
</plurals>
<string name="screen_app_lock_use_biometric_android">"Použijte biometrické údaje"</string>
<string name="screen_app_lock_use_pin_android">"Použít PIN"</string>
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
<string name="screen_signout_in_progress_dialog_content">"Odebírání zařízení…"</string>
</resources>

View file

@ -23,7 +23,7 @@ Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Indtast venligst den samme PIN-kode to gange"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-koderne stemmer ikke overens"</string>
<string name="screen_app_lock_signout_alert_message">"Du vil være nødt til at logge ind igen og oprette en ny PIN-kode for at fortsætte."</string>
<string name="screen_app_lock_signout_alert_title">"Du bliver logget ud"</string>
<string name="screen_app_lock_signout_alert_title">"Denne enhed bliver fjernet"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"Du har %1$d forsøg på at låse op"</item>
<item quantity="other">"Du har %1$d forsøg på at låse op"</item>
@ -34,5 +34,5 @@ Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af
</plurals>
<string name="screen_app_lock_use_biometric_android">"Brug biometri"</string>
<string name="screen_app_lock_use_pin_android">"Brug PIN-kode"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ud…"</string>
<string name="screen_signout_in_progress_dialog_content">"Fjerner enhed…"</string>
</resources>

View file

@ -23,7 +23,7 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Adja meg a PIN-kódját kétszer"</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"A PIN-kódok nem egyeznek"</string>
<string name="screen_app_lock_signout_alert_message">"A folytatáshoz újra be kell jelentkeznie, és létre kell hoznia egy új PIN-kódot"</string>
<string name="screen_app_lock_signout_alert_title">"Kijelentkeztetésre kerül"</string>
<string name="screen_app_lock_signout_alert_title">"Ez az eszköz eltávolításra kerül"</string>
<plurals name="screen_app_lock_subtitle">
<item quantity="one">"%1$d próbálkozása van a feloldáshoz"</item>
<item quantity="other">"%1$d próbálkozása van a feloldáshoz"</item>
@ -34,5 +34,5 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele
</plurals>
<string name="screen_app_lock_use_biometric_android">"Biometrikus adatok használata"</string>
<string name="screen_app_lock_use_pin_android">"PIN-kód használata"</string>
<string name="screen_signout_in_progress_dialog_content">"Kijelentkezés…"</string>
<string name="screen_signout_in_progress_dialog_content">"Eszköz eltávolítása…"</string>
</resources>

View file

@ -36,5 +36,5 @@
</plurals>
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
<string name="screen_signout_in_progress_dialog_content">"Удаление устройства…"</string>
</resources>

View file

@ -34,5 +34,5 @@ Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från a
</plurals>
<string name="screen_app_lock_use_biometric_android">"Använd biometri"</string>
<string name="screen_app_lock_use_pin_android">"Använd PIN-kod"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
<string name="screen_signout_in_progress_dialog_content">"Tar bort enhet …"</string>
</resources>

View file

@ -4,17 +4,29 @@
<string name="screen_account_provider_form_hint">"Pagrindinio serverio adresas"</string>
<string name="screen_account_provider_form_notice">"Įveskite paieškos terminą arba domeno adresą."</string>
<string name="screen_account_provider_form_subtitle">"Ieškokite bendrovės, bendruomenės arba privataus serverio."</string>
<string name="screen_account_provider_form_title">"Rasti paskyros teikėją"</string>
<string name="screen_account_provider_form_title">"Raskite paskyros teikėją"</string>
<string name="screen_account_provider_signin_subtitle">"Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus."</string>
<string name="screen_account_provider_signin_title">"Ketinate prisijungti prie %s"</string>
<string name="screen_account_provider_signup_subtitle">"Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus."</string>
<string name="screen_account_provider_signup_title">"Ketinate sukurti paskyrą teikėjoje %s"</string>
<string name="screen_change_account_provider_other">"Kita"</string>
<string name="screen_change_account_provider_subtitle">"Naudokite skirtingą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą."</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org tai didelis nemokamas serveris viešajame „Matrix“ tinkle saugiam ir decentralizuotam bendravimui, kurį valdo „Matrix.org“ fondas."</string>
<string name="screen_change_account_provider_other">"Kitas"</string>
<string name="screen_change_account_provider_subtitle">"Naudokite kitą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą."</string>
<string name="screen_change_account_provider_title">"Keisti paskyros teikėją"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nepavyko pasiekti šio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administracija dėl tolimesnės pagalbos."</string>
<string name="screen_change_server_form_header">"Serverio URL"</string>
<string name="screen_change_server_subtitle">"Koks yra Jūsų serverio adresas?"</string>
<string name="screen_change_server_error_element_pro_required_action_android">"„Google Play“"</string>
<string name="screen_change_server_error_element_pro_required_message">"„Element Pro“ programa privaloma teikėjoje %1$s. Atsisiųskite ją iš parduotuvės."</string>
<string name="screen_change_server_error_element_pro_required_title">"„Element Pro“ privaloma"</string>
<string name="screen_change_server_error_invalid_homeserver">"Nepavyko pasiekti šio pagrindinio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administratoriumi dėl tolimesnės pagalbos."</string>
<string name="screen_change_server_error_invalid_well_known">"Serveris nepasiekiamas dėl problemos .labai-zinomame faile:
%1$s"</string>
<string name="screen_change_server_error_no_sliding_sync_message">"Pasirinktas paskyros teikėjas nepalaiko slankiojo sinchronizavimo. Norint naudoti „%1$s“, reikia atnaujinti serverį."</string>
<string name="screen_change_server_error_unauthorized_homeserver">"„%1$s“ neleidžiama prisijungti prie %2$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_content">"Ši programa sukonfigūruota, kad leistų %1$s."</string>
<string name="screen_change_server_error_unauthorized_homeserver_title">"Paskyros teikėjas %1$s neleidžiamas."</string>
<string name="screen_change_server_form_header">"Pagrindinio serverio URL"</string>
<string name="screen_change_server_form_notice">"Įveskite domeno adresą."</string>
<string name="screen_change_server_subtitle">"Koks yra jūsų serverio adresas?"</string>
<string name="screen_change_server_title">"Pasirinkite savo serverį"</string>
<string name="screen_create_account_title">"Kurti paskyrą"</string>
<string name="screen_login_error_deactivated_account">"Ši paskyra buvo išjungta."</string>
<string name="screen_login_error_invalid_credentials">"Neteisingas vartotojo vardas ir (arba) slaptažodis"</string>
@ -31,7 +43,7 @@
<string name="screen_onboarding_sign_up">"Kurti paskyrą"</string>
<string name="screen_onboarding_welcome_message">"Sveiki atvykę į sparčiausią „%1$s“ kada nors. Pagerintas spartai ir paprastumui."</string>
<string name="screen_onboarding_welcome_subtitle">"Sveiki atvykę į „%1$s“. Pagerintas spartai ir paprastumui."</string>
<string name="screen_onboarding_welcome_title">"Būkite savo elemente"</string>
<string name="screen_onboarding_welcome_title">"Būkite savo stichijoje"</string>
<string name="screen_server_confirmation_change_server">"Keisti paskyros teikėją"</string>
<string name="screen_server_confirmation_message_login_element_dot_io">"Privatus serveris “Element” darbuotojams."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui."</string>

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Opravdu se chcete odhlásit?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Odhlásit se"</string>
<string name="screen_signout_confirmation_dialog_title">"Odhlásit se"</string>
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
<string name="screen_signout_key_backup_disabled_title">"Vypnuli jste zálohování"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Když jste přešli do režimu offline, vaše klíče se ještě stále zálohovaly. Znovu se připojte, aby bylo možné před odhlášením zálohovat vaše klíče."</string>
<string name="screen_signout_confirmation_dialog_content">"Opravdu chcete odstranit toto zařízení?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Odebrat toto zařízení"</string>
<string name="screen_signout_confirmation_dialog_title">"Odebrat toto zařízení"</string>
<string name="screen_signout_in_progress_dialog_content">"Odebírání zařízení…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty."</string>
<string name="screen_signout_key_backup_disabled_title">"Chystáte se ztratit přístup ke svým šifrovaným chatům"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Vaše klíče se stále zálohovaly, když jste byli offline. Před odpojením tohoto zařízení se znovu připojte, aby se vaše klíče mohly zálohovat."</string>
<string name="screen_signout_key_backup_offline_title">"Vaše klíče jsou stále zálohovány"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Před odhlášením prosím počkejte na dokončení."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Před odstraněním tohoto zařízení počkejte, až se proces dokončí."</string>
<string name="screen_signout_key_backup_ongoing_title">"Vaše klíče jsou stále zálohovány"</string>
<string name="screen_signout_preference_item">"Odhlásit se"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
<string name="screen_signout_recovery_disabled_title">"Obnovení není nastaveno"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám."</string>
<string name="screen_signout_save_recovery_key_title">"Uložili jste si klíč pro obnovení?"</string>
<string name="screen_signout_preference_item">"Odebrat toto zařízení"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty."</string>
<string name="screen_signout_recovery_disabled_title">"Chystáte se ztratit přístup ke svým šifrovaným chatům"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty."</string>
<string name="screen_signout_save_recovery_key_title">"Před odebráním tohoto zařízení se ujistěte, že máte přístup ke klíči pro obnovení"</string>
</resources>

View file

@ -1,17 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Er du sikker på, at du vil logge ud?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Log ud"</string>
<string name="screen_signout_confirmation_dialog_title">"Log ud"</string>
<string name="screen_signout_in_progress_dialog_content">"Logger ud…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger ud nu, mister du adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har slået sikkerhedskopiering fra"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du logger ud."</string>
<string name="screen_signout_confirmation_dialog_content">"Er du sikker på, at ønsker at fjerne denne enhed?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Fjern denne enhed"</string>
<string name="screen_signout_confirmation_dialog_title">"Fjern denne enhed"</string>
<string name="screen_signout_in_progress_dialog_content">"Fjerner enhed…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind."</string>
<string name="screen_signout_key_backup_disabled_title">"Du er ved at miste adgangen til dine krypterede chats"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du fjerner denne enhed."</string>
<string name="screen_signout_key_backup_offline_title">"Dine nøgler bliver stadig sikkerhedskopieret"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vent på, at dette er fuldført, før du logger ud."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vent venligst, indtil dette er færdigt, før du fjerner denne enhed."</string>
<string name="screen_signout_key_backup_ongoing_title">"Dine nøgler bliver stadig sikkerhedskopieret"</string>
<string name="screen_signout_preference_item">"Log ud"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger af nu, mister du adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_recovery_disabled_title">"Gendannelse er ikke konfigureret"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du er ved at logge ud af din sidste session. Hvis du logger af nu, kan du miste adgangen til dine krypterede meddelelser."</string>
<string name="screen_signout_preference_item">"Fjern denne enhed"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind."</string>
<string name="screen_signout_recovery_disabled_title">"Du er ved at miste adgangen til dine krypterede chats"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind."</string>
<string name="screen_signout_save_recovery_key_title">"Sørg for, at du har adgang til din gendannelsesnøgle, før du fjerner denne enhed."</string>
</resources>

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Biztos, hogy kijelentkezik?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Kijelentkezés"</string>
<string name="screen_signout_confirmation_dialog_title">"Kijelentkezés"</string>
<string name="screen_signout_in_progress_dialog_content">"Kijelentkezés…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_key_backup_disabled_title">"Kikapcsolta a biztonsági mentést"</string>
<string name="screen_signout_key_backup_offline_subtitle">"A kulcsai mentése során bontotta a kapcsolatot. Kapcsolódjon újra, hogy a kulcsai továbbra is mentésre kerüljenek mielőtt kijelentkezik."</string>
<string name="screen_signout_confirmation_dialog_content">"Biztosan eltávolítja ezt az eszközt?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Eszköz eltávolítása"</string>
<string name="screen_signout_confirmation_dialog_title">"Eszköz eltávolítása"</string>
<string name="screen_signout_in_progress_dialog_content">"Eszköz eltávolítása…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához."</string>
<string name="screen_signout_key_backup_disabled_title">"Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez"</string>
<string name="screen_signout_key_backup_offline_subtitle">"A kulcsok biztonsági mentése még folyamatban volt, amikor megszűnt a hálózati kapcsolat. Csatlakozzon újra, hogy a kulcsok biztonsági mentése megtörténhessen, mielőtt eltávolítja ezt az eszközt."</string>
<string name="screen_signout_key_backup_offline_title">"A kulcsai mentése még folyamatban van"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Kijelentkezés előtt várja meg a befejezését."</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Várja meg, amíg ez befejeződik, mielőtt eltávolítja ezt az eszközt."</string>
<string name="screen_signout_key_backup_ongoing_title">"A kulcsai mentése még folyamatban van"</string>
<string name="screen_signout_preference_item">"Kijelentkezés"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_recovery_disabled_title">"A helyreállítás nincs beállítva"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez."</string>
<string name="screen_signout_save_recovery_key_title">"Mentette a helyreállítási kulcsát?"</string>
<string name="screen_signout_preference_item">"Eszköz eltávolítása"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához."</string>
<string name="screen_signout_recovery_disabled_title">"Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához."</string>
<string name="screen_signout_save_recovery_key_title">"Az eszköz eltávolítása előtt győződjön meg arról, hogy hozzáfér a helyreállítási kulcshoz"</string>
</resources>

View file

@ -3,7 +3,7 @@
<string name="screen_signout_confirmation_dialog_content">"Вы уверены, что вы хотите выйти?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Удалить это устройство"</string>
<string name="screen_signout_confirmation_dialog_title">"Удалить это устройство"</string>
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
<string name="screen_signout_in_progress_dialog_content">"Удаление устройства…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."</string>
<string name="screen_signout_key_backup_disabled_title">"Вы отключили резервное копирование"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей."</string>

View file

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_confirmation_dialog_content">"Är du säker på att du vill logga ut?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Logga ut"</string>
<string name="screen_signout_confirmation_dialog_title">"Logga ut"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
<string name="screen_signout_confirmation_dialog_content">"Är du säker på att du vill ta bort den här enheten?"</string>
<string name="screen_signout_confirmation_dialog_submit">"Ta bort den här enheten"</string>
<string name="screen_signout_confirmation_dialog_title">"Ta bort den här enheten"</string>
<string name="screen_signout_in_progress_dialog_content">"Tar bort enhet …"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har stängt av säkerhetskopiering"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut."</string>
<string name="screen_signout_key_backup_offline_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vänta tills detta är klart innan du loggar ut."</string>
<string name="screen_signout_key_backup_ongoing_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_preference_item">"Logga ut"</string>
<string name="screen_signout_preference_item">"Ta bort den här enheten"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_recovery_disabled_title">"Återställning inte inställd"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden."</string>

View file

@ -38,6 +38,7 @@ interface MessagesEntryPoint : FeatureEntryPoint {
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean)
fun navigateToRoom(roomId: RoomId)
fun navigateToDeveloperSettings()
}
data class Params(val initialTarget: InitialTarget) : NodeInputs

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.recentemojis.api)
implementation(projects.libraries.roomselect.api)
implementation(projects.libraries.slashcommands.api)
implementation(projects.libraries.audio.api)
implementation(projects.libraries.voiceplayer.api)
implementation(projects.libraries.voicerecorder.api)
@ -104,4 +105,5 @@ dependencies {
testImplementation(projects.features.poll.test)
testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.recentemojis.test)
testImplementation(projects.libraries.slashcommands.test)
}

View file

@ -293,6 +293,10 @@ class MessagesFlowNode(
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
}
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
}
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
@ -428,6 +432,10 @@ class MessagesFlowNode(
override fun handleForwardEventClick(eventId: EventId) {
backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true))
}
override fun navigateToThread(threadRootId: ThreadId) {
backstack.push(NavTarget.Thread(threadRootId, null))
}
}
createNode<PinnedMessagesListNode>(buildContext, plugins = listOf(callback))
}
@ -502,6 +510,10 @@ class MessagesFlowNode(
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
backstack.push(NavTarget.Thread(threadRootId, focusedEventId))
}
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
}
createNode<ThreadedMessagesNode>(buildContext, listOf(inputs, callback))
}

View file

@ -23,6 +23,8 @@ interface MessagesNavigator {
fun navigateToEditPoll(eventId: EventId)
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>)
fun navigateToMember(userId: UserId)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToDeveloperSettings()
fun close()
}

View file

@ -105,7 +105,7 @@ class MessagesNode(
private val timelineController = TimelineController(room, room.liveTimeline)
private val presenter = presenterFactory.create(
navigator = this,
composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = false),
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
actionListPresenter = actionListPresenterFactory.create(
postProcessor = TimelineItemActionPostProcessor.Default,
@ -130,6 +130,7 @@ class MessagesNode(
fun navigateToRoomDetails()
fun navigateToPinnedMessagesList()
fun navigateToKnockRequestsList()
fun navigateToDeveloperSettings()
}
override fun onBuilt() {
@ -222,10 +223,18 @@ class MessagesNode(
}
}
override fun navigateToMember(userId: UserId) {
callback.navigateToRoomMemberDetails(userId)
}
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
callback.navigateToThread(threadRootId, focusedEventId)
}
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
private fun displaySameRoomToast() {
context.toast(CommonStrings.screen_room_permalink_same_room_android)
}

View file

@ -250,12 +250,11 @@ class MessagesPresenter(
is MessagesEvent.OnUserClicked -> {
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
}
is MessagesEvent.MarkAsFullyReadAndExit -> coroutineScope.launch {
if (!markingAsReadAndExiting.getAndSet(true)) {
is MessagesEvent.MarkAsFullyReadAndExit -> if (!markingAsReadAndExiting.getAndSet(true)) {
coroutineScope.launch {
val latestEventId = room.liveTimeline.getLatestEventId().getOrElse {
Timber.w(it, "Failed to get latest event id to mark as fully read")
navigator.close()
return@launch
null
}
latestEventId?.let { eventId ->
sessionCoroutineScope.launch {
@ -263,7 +262,6 @@ class MessagesPresenter(
}
}
navigator.close()
markingAsReadAndExiting.set(false)
}
}
}

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@ -464,6 +465,9 @@ private fun MessagesViewContent(
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior(
pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0,
)
val density = LocalDensity.current
var pinnedBannerHeightDp by remember { mutableStateOf(0.dp) }
TimelineView(
state = state.timelineState,
timelineProtectionState = state.timelineProtectionState,
@ -479,11 +483,13 @@ private fun MessagesViewContent(
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
onJoinCallClick = onJoinCallClick,
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
floatingDateTopOffset = pinnedBannerHeightDp,
)
if (state.timelineState.timelineMode !is Timeline.Mode.Thread) {
AnimatedVisibility(
visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
modifier = Modifier.onSizeChanged { pinnedBannerHeightDp = with(density) { it.height.toDp() } },
enter = expandVertically(),
exit = shrinkVertically(),
) {

View file

@ -36,4 +36,5 @@ sealed interface MessageComposerEvent {
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvent
data class InsertSuggestion(val resolvedSuggestion: ResolvedSuggestion) : MessageComposerEvent
data object SaveDraft : MessageComposerEvent
data object ClearSlashError : MessageComposerEvent
}

View file

@ -15,6 +15,7 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -33,12 +34,14 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.location.api.LocationService
import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.Attachment.Media
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.draft.ComposerDraftService
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
@ -68,6 +71,9 @@ import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.slashcommands.api.SlashCommand
import io.element.android.libraries.slashcommands.api.SlashCommandService
import io.element.android.libraries.slashcommands.api.message
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
@ -104,6 +110,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
class MessageComposerPresenter(
@Assisted private val navigator: MessagesNavigator,
@Assisted private val timelineController: TimelineController,
@Assisted private val isInThread: Boolean,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val room: JoinedRoom,
private val mediaPickerProvider: PickerProvider,
@ -125,10 +132,15 @@ class MessageComposerPresenter(
private val suggestionsProcessor: SuggestionsProcessor,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val notificationConversationService: NotificationConversationService,
private val slashCommandService: SlashCommandService,
) : Presenter<MessageComposerState> {
@AssistedFactory
interface Factory {
fun create(timelineController: TimelineController, navigator: MessagesNavigator): MessageComposerPresenter
fun create(
timelineController: TimelineController,
navigator: MessagesNavigator,
isInThread: Boolean,
): MessageComposerPresenter
}
private val mediaSender = mediaSenderFactory.create(timelineMode = timelineController.mainTimelineMode())
@ -218,6 +230,8 @@ class MessageComposerPresenter(
}
)
val slashCommandAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
LaunchedEffect(Unit) {
val draft = draftService.loadDraft(
roomId = room.roomId,
@ -246,12 +260,13 @@ class MessageComposerPresenter(
sessionCoroutineScope.sendMessage(
markdownTextEditorState = markdownTextEditorState,
richTextEditorState = richTextEditorState,
slashCommandAction = slashCommandAction,
)
}
is MessageComposerEvent.SendUri -> {
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
sessionCoroutineScope.sendAttachment(
attachment = Attachment.Media(
attachment = Media(
localMedia = localMediaFactory.createFromUri(
uri = event.uri,
mimeType = null,
@ -340,6 +355,9 @@ class MessageComposerPresenter(
val link = permalinkBuilder.permalinkForRoomAlias(suggestion.roomAlias).getOrNull() ?: return@launch
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
}
is ResolvedSuggestion.Command -> {
richTextEditorState.replaceSuggestion(suggestion.command.command)
}
}
} else if (markdownTextEditorState.currentSuggestion != null) {
markdownTextEditorState.insertSuggestion(
@ -354,6 +372,9 @@ class MessageComposerPresenter(
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
sessionCoroutineScope.updateDraft(draft, isVolatile = false)
}
MessageComposerEvent.ClearSlashError -> {
slashCommandAction.value = AsyncAction.Uninitialized
}
}
}
@ -385,6 +406,7 @@ class MessageComposerPresenter(
suggestions = suggestions.toImmutableList(),
resolveMentionDisplay = resolveMentionDisplay,
resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay,
slashCommandAction = slashCommandAction.value,
eventSink = ::handleEvent,
)
}
@ -422,6 +444,7 @@ class MessageComposerPresenter(
roomAliasSuggestions = roomAliasSuggestions,
currentUserId = currentUserId,
canSendRoomMention = ::canSendRoomMention,
isInThread = isInThread,
)
suggestions.clear()
suggestions.addAll(result)
@ -433,9 +456,69 @@ class MessageComposerPresenter(
private fun CoroutineScope.sendMessage(
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
slashCommandAction: MutableState<AsyncAction<Unit>>,
) = launch {
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true)
val capturedMode = messageComposerContext.composerMode
val slashCommand = if (capturedMode is MessageComposerMode.Normal) {
slashCommandService.parse(
textMessage = message.markdown,
formattedMessage = message.html,
isInThreadTimeline = isInThread,
)
} else {
SlashCommand.NotACommand
}
when (slashCommand) {
is SlashCommand.NotACommand -> Unit
is SlashCommand.Error -> {
slashCommandAction.value = AsyncAction.Failure(Exception(slashCommand.message()))
return@launch
}
is SlashCommand.SlashCommandNavigation -> {
when (slashCommand) {
is SlashCommand.ShowUser -> {
navigator.navigateToMember(slashCommand.userId)
}
SlashCommand.DevTools -> {
navigator.navigateToDeveloperSettings()
}
}
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
return@launch
}
is SlashCommand.SlashCommandSendMessage -> {
timelineController.invokeOnCurrentTimeline {
slashCommandService.proceedSendMessage(slashCommand, this)
.onFailure { cause ->
Timber.e(cause, "Failed to proceed with admin slash command")
slashCommandAction.value = AsyncAction.Failure(cause)
}
.onSuccess {
// Reset composer
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
}
}
return@launch
}
is SlashCommand.SlashCommandAdmin -> {
slashCommandAction.value = AsyncAction.Loading
slashCommandService.proceedAdmin(slashCommand)
.onFailure { cause ->
Timber.e(cause, "Failed to proceed with admin slash command")
slashCommandAction.value = AsyncAction.Failure(cause)
}
.onSuccess {
// Reset composer
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
slashCommandAction.value = AsyncAction.Uninitialized
}
return@launch
}
}
// Reset composer right away
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
when (capturedMode) {

View file

@ -9,6 +9,7 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Stable
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
@ -26,5 +27,6 @@ data class MessageComposerState(
val suggestions: ImmutableList<ResolvedSuggestion>,
val resolveMentionDisplay: (String, String) -> TextDisplay,
val resolveAtRoomMentionDisplay: () -> TextDisplay,
val slashCommandAction: AsyncAction<Unit>,
val eventSink: (MessageComposerEvent) -> Unit,
)

View file

@ -9,6 +9,7 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.TextEditorState
@ -32,6 +33,7 @@ fun aMessageComposerState(
showAttachmentSourcePicker: Boolean = false,
canShareLocation: Boolean = true,
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
slashCommandAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (MessageComposerEvent) -> Unit = {},
) = MessageComposerState(
textEditorState = textEditorState,
@ -43,5 +45,6 @@ fun aMessageComposerState(
suggestions = suggestions,
resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
resolveAtRoomMentionDisplay = { TextDisplay.Plain },
slashCommandAction = slashCommandAction,
eventSink = eventSink,
)

View file

@ -22,6 +22,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.TextComposer
@ -115,6 +116,12 @@ internal fun MessageComposerView(
onTyping = ::onTyping,
onSelectRichContent = ::sendUri,
)
AsyncActionView(
async = state.slashCommandAction,
onSuccess = {},
onErrorDismiss = { state.eventSink(MessageComposerEvent.ClearSlashError) },
)
}
@PreviewsDayNight

View file

@ -29,6 +29,7 @@ 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.avatar.AvatarType.Room
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -63,6 +65,7 @@ fun SuggestionsPickerView(
is ResolvedSuggestion.AtRoom -> "@room"
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
is ResolvedSuggestion.Alias -> suggestion.roomId.value
is ResolvedSuggestion.Command -> suggestion.command.command
}
}
) {
@ -91,54 +94,81 @@ private fun SuggestionItemView(
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.clickable { onSelectSuggestion(suggestion) },
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
.clickable { onSelectSuggestion(suggestion) }
.padding(horizontal = 16.dp),
) {
val avatarSize = AvatarSize.Suggestion
val avatarData = when (suggestion) {
is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
is ResolvedSuggestion.Command -> null
}
val avatarType = when (suggestion) {
is ResolvedSuggestion.Alias -> AvatarType.Room()
is ResolvedSuggestion.Alias -> Room()
ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member -> AvatarType.User
is ResolvedSuggestion.Command -> null
}
val title = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
is ResolvedSuggestion.Alias -> suggestion.roomName
is ResolvedSuggestion.Command -> suggestion.command.command
}
val details = when (suggestion) {
is ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member,
is ResolvedSuggestion.Alias -> null
is ResolvedSuggestion.Command -> suggestion.command.parameters
}
val subtitle = when (suggestion) {
is ResolvedSuggestion.AtRoom -> "@room"
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
is ResolvedSuggestion.Alias -> suggestion.roomAlias.value
is ResolvedSuggestion.Command -> suggestion.command.description
}
if (avatarData != null && avatarType != null) {
Avatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 16.dp),
)
}
Avatar(
avatarData = avatarData,
avatarType = avatarType,
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp, top = 8.dp, bottom = 8.dp)
.padding(top = 8.dp, bottom = 8.dp)
.align(Alignment.CenterVertically),
) {
title?.let {
Text(
text = it,
style = ElementTheme.typography.fontBodyLgRegular,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
title?.let {
Text(
text = it,
style = ElementTheme.typography.fontBodyLgRegular,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
details?.let {
Text(
text = it,
style = ElementTheme.typography.fontBodyMdRegular,
maxLines = 1,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
)
}
}
Text(
text = subtitle,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
@ -174,7 +204,21 @@ internal fun SuggestionsPickerViewPreview() {
roomId = RoomId("!room:matrix.org"),
roomName = "My room",
roomAvatarUrl = null,
)
),
ResolvedSuggestion.Command(
command = SlashCommandSuggestion(
command = "/noparam",
parameters = null,
description = "A slash command without parameters",
)
),
ResolvedSuggestion.Command(
command = SlashCommandSuggestion(
command = "/withparam",
parameters = "<user-id> [reason]",
description = "A slash command with parameters",
)
),
),
onSelectSuggestion = {}
)

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.slashcommands.api.SlashCommandService
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType
@ -23,7 +24,9 @@ import io.element.android.libraries.textcomposer.model.SuggestionType
* This class is responsible for processing suggestions when `@`, `/` or `#` are type in the composer.
*/
@Inject
class SuggestionsProcessor {
class SuggestionsProcessor(
private val slashCommandService: SlashCommandService,
) {
/**
* Process the suggestion.
* @param suggestion The current suggestion input
@ -31,6 +34,7 @@ class SuggestionsProcessor {
* @param roomAliasSuggestions The available room alias suggestions
* @param currentUserId The current user id
* @param canSendRoomMention Should return true if the current user can send room mentions
* @param isInThread Whether the composer is in a thread or not, used to filter slash commands suggestions
* @return The list of suggestions to display
*/
suspend fun process(
@ -39,6 +43,7 @@ class SuggestionsProcessor {
roomAliasSuggestions: List<RoomAliasSuggestion>,
currentUserId: UserId,
canSendRoomMention: suspend () -> Boolean,
isInThread: Boolean,
): List<ResolvedSuggestion> {
suggestion ?: return emptyList()
return when (suggestion.type) {
@ -69,7 +74,16 @@ class SuggestionsProcessor {
)
}
}
SuggestionType.Command,
SuggestionType.Command -> {
// Command suggestions are valid only if this is the beginning of the message
if (suggestion.start == 0) {
slashCommandService.getSuggestions(suggestion.text, isInThread).map {
ResolvedSuggestion.Command(it)
}
} else {
emptyList()
}
}
SuggestionType.Emoji,
is SuggestionType.Custom -> {
// Clear suggestions

View file

@ -10,7 +10,9 @@ package io.element.android.features.messages.impl.pinned.list
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.ThreadId
sealed interface PinnedMessagesListEvent {
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvent
data class OpenThread(val threadRootId: ThreadId) : PinnedMessagesListEvent
}

View file

@ -9,10 +9,12 @@
package io.element.android.features.messages.impl.pinned.list
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
interface PinnedMessagesListNavigator {
fun viewInTimeline(eventId: EventId)
fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
fun forwardEvent(eventId: EventId)
fun navigateToThread(threadRootId: ThreadId)
}

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@ -55,6 +56,7 @@ class PinnedMessagesListNode(
fun handlePermalinkClick(data: PermalinkData.RoomLink)
fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo)
fun handleForwardEventClick(eventId: EventId)
fun navigateToThread(threadRootId: ThreadId)
}
private val callback: Callback = callback()
@ -95,6 +97,10 @@ class PinnedMessagesListNode(
callback.handleForwardEventClick(eventId)
}
override fun navigateToThread(threadRootId: ThreadId) {
callback.navigateToThread(threadRootId)
}
@Composable
override fun View(modifier: Modifier) {
CompositionLocalProvider(

View file

@ -137,6 +137,7 @@ class PinnedMessagesListPresenter(
fun handleEvent(event: PinnedMessagesListEvent) {
when (event) {
is PinnedMessagesListEvent.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event)
is PinnedMessagesListEvent.OpenThread -> navigator.navigateToThread(event.threadRootId)
}
}

View file

@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.link.LinkEvent
import io.element.android.features.messages.impl.link.LinkView
import io.element.android.features.messages.impl.timeline.TimelineEvent
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
@ -235,7 +236,12 @@ private fun PinnedMessagesListLoaded(
onReadReceiptClick = {},
onSwipeToReply = {},
onJoinCallClick = {},
eventSink = {},
eventSink = { timelineItemEvent ->
when (timelineItemEvent) {
is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId))
else -> Unit
}
},
eventContentView = { event, contentModifier, onContentLayoutChange ->
TimelineItemEventContentViewWrapper(
event = event,

View file

@ -112,7 +112,7 @@ class ThreadedMessagesNode(
this.timelineController = timelineController
return presenterFactory.create(
navigator = this,
composerPresenter = messageComposerPresenterFactory.create(timelineController, this),
composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = true),
timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this),
// TODO add special processor for threaded timeline
actionListPresenter = actionListPresenterFactory.create(
@ -136,6 +136,7 @@ class ThreadedMessagesNode(
fun navigateToEditPoll(eventId: EventId)
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToDeveloperSettings()
}
override fun onBuilt() {
@ -233,10 +234,18 @@ class ThreadedMessagesNode(
callback.handlePermalinkClick(permalinkData)
}
override fun navigateToMember(userId: UserId) {
callback.navigateToRoomMemberDetails(userId)
}
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
callback.navigateToThread(threadRootId, focusedEventId)
}
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
override fun close() = navigateUp()
@Composable

View file

@ -149,6 +149,9 @@ class TimelinePresenter(
val displayThreadSummaries by produceState(false) {
value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
}
val displayFloatingDateBadge by produceState(false) {
value = featureFlagService.isFeatureEnabled(FeatureFlags.FloatingDateBadge)
}
fun handleEvent(event: TimelineEvent) {
when (event) {
@ -316,6 +319,7 @@ class TimelinePresenter(
messageShieldDialogData = messageShieldDialogData.value,
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
displayThreadSummaries = displayThreadSummaries,
displayFloatingDateBadge = displayFloatingDateBadge,
eventSink = ::handleEvent,
)
}

View file

@ -34,6 +34,7 @@ data class TimelineState(
val messageShieldDialogData: MessageShieldData?,
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
val displayThreadSummaries: Boolean,
val displayFloatingDateBadge: Boolean,
val eventSink: (TimelineEvent) -> Unit,
) {
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event

View file

@ -56,6 +56,7 @@ fun aTimelineState(
messageShield: MessageShield? = null,
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
displayThreadSummaries: Boolean = false,
displayFloatingDateBadge: Boolean = false,
eventSink: (TimelineEvent) -> Unit = {},
): TimelineState {
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
@ -75,6 +76,7 @@ fun aTimelineState(
messageShieldDialogData = messageShield?.let { MessageShieldData(it) },
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
displayThreadSummaries = displayThreadSummaries,
displayFloatingDateBadge = displayFloatingDateBadge,
eventSink = eventSink,
)
}

View file

@ -47,10 +47,12 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
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.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView
import io.element.android.features.messages.impl.timeline.components.FloatingDateBadgeOverlay
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
import io.element.android.features.messages.impl.timeline.components.toText
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
@ -105,6 +107,7 @@ fun TimelineView(
lazyListState: LazyListState = rememberLazyListState(),
forceJumpToBottomVisibility: Boolean = false,
nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
floatingDateTopOffset: Dp = 0.dp,
) {
fun clearFocusRequestState() {
state.eventSink(TimelineEvent.ClearFocusRequestState)
@ -210,6 +213,15 @@ fun TimelineView(
onJumpToLive = ::onJumpToLive,
onFocusEventRender = ::onFocusEventRender,
)
if (state.displayFloatingDateBadge && useReverseLayout) {
FloatingDateBadgeOverlay(
lazyListState = lazyListState,
timelineItems = state.timelineItems,
isLive = state.isLive,
topOffset = floatingDateTopOffset,
)
}
}
}

View file

@ -0,0 +1,144 @@
/*
* Copyright (c) 2025 Element Creations 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.messages.impl.timeline.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.floatingDateBadgeBackground
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlin.time.Duration.Companion.milliseconds
@Composable
internal fun BoxScope.FloatingDateBadgeOverlay(
lazyListState: LazyListState,
timelineItems: ImmutableList<TimelineItem>,
isLive: Boolean,
topOffset: Dp = 0.dp,
) {
// This needs to be a state to trigger a `derivedState` recalculation
val updatedTimelineItems by rememberUpdatedState(timelineItems)
// Look for the last visible item with a timestamp, starting from the last visible item and going backwards until we find one or reach the start of the list
val lastVisibleItemWithTimestamp by remember {
derivedStateOf {
var index = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf null
while (index >= 0) {
when (val item = updatedTimelineItems.getOrNull(index)) {
is TimelineItem.Event -> return@derivedStateOf item
is TimelineItem.Virtual -> if (item.model is TimelineItemDaySeparatorModel) return@derivedStateOf item
is TimelineItem.GroupedEvents -> return@derivedStateOf item.events.firstOrNull()
null -> Unit
}
index--
}
null
}
}
// Store the formatted date so we recompute it lazily and can keep it around even if we need to dispose the badge because the timeline items changed
var formattedDate: String? by remember { mutableStateOf(null) }
// Update the formatted date when we have a new non-null timestamp
LaunchedEffect(lastVisibleItemWithTimestamp) {
lastVisibleItemWithTimestamp?.formattedDate()?.let { formattedDate = it }
}
val isAtBottom by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex < 3 && isLive
}
}
var isBadgeVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
snapshotFlow { lazyListState.isScrollInProgress }
.collectLatest { isScrolling ->
if (isScrolling) {
isBadgeVisible = true
} else {
delay(2000.milliseconds)
isBadgeVisible = false
}
}
}
val showBadge = isBadgeVisible && !isAtBottom && formattedDate != null
AnimatedVisibility(
visible = showBadge,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 8.dp + topOffset),
enter = fadeIn(animationSpec = tween(150)),
exit = fadeOut(animationSpec = tween(300)),
) {
formattedDate?.let { dateText ->
FloatingDateBadge(
modifier = Modifier.padding(8.dp),
dateText = dateText,
)
}
}
}
@Composable
internal fun FloatingDateBadge(
dateText: String,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
color = ElementTheme.colors.floatingDateBadgeBackground,
shadowElevation = 4.dp,
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
text = dateText,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
}
}
@PreviewsDayNight
@Composable
internal fun FloatingDateBadgePreview() = ElementPreview {
Box(modifier = Modifier.padding(16.dp)) {
FloatingDateBadge(dateText = "March 9, 2026")
}
}

View file

@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.components
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
@ -745,11 +746,17 @@ private fun MessageEventBubbleContent(
} else {
inReplyToModifier.clickable(onClick = inReplyToClick)
}
InReplyToView(
inReplyTo = inReplyTo,
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
modifier = talkbackCompatModifier,
)
Box(
modifier = talkbackCompatModifier
.border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp))
.background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(6.dp))
.padding(4.dp)
) {
InReplyToView(
inReplyTo = inReplyTo,
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
)
}
}
if (inReplyToDetails != null) {
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
@ -837,7 +844,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
threadInfo = TimelineItemThreadInfo.ThreadRoot(
latestEventText = "This is the latest message in the thread",
summary = ThreadSummary(
AsyncData.Success(
latestEvent = AsyncData.Success(
EmbeddedEventInfo(
eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")),
content = MessageContent(
@ -855,7 +862,8 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
),
timestamp = 0L,
)
), numberOfReplies = 20L
),
numberOfReplies = 20L,
)
)
),

View file

@ -66,6 +66,11 @@ class TimelineItemEventFactory(
timestamp = currentTimelineItem.event.timestamp,
mode = DateFormatterMode.TimeOnly,
)
val sentDate = dateFormatter.format(
timestamp = currentTimelineItem.event.timestamp,
mode = DateFormatterMode.Day,
useRelative = true,
)
val senderAvatarData = AvatarData(
id = currentSender.value,
name = senderProfile.getDisambiguatedDisplayName(currentSender),
@ -108,6 +113,7 @@ class TimelineItemEventFactory(
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
sentTimeMillis = currentTimelineItem.event.timestamp,
sentTime = sentTime,
sentDate = sentDate,
groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState(),
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),

View file

@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.EventId
@ -59,6 +60,12 @@ sealed interface TimelineItem {
is GroupedEvents -> "groupedEvent"
}
fun formattedDate(): String? = when (this) {
is Event -> sentDate.takeIf { it.isNotEmpty() }
is Virtual -> (model as? TimelineItemDaySeparatorModel)?.formattedDate?.takeIf { it.isNotEmpty() }
is GroupedEvents -> null
}
data class Virtual(
val id: UniqueId,
val model: TimelineItemVirtualModel
@ -75,6 +82,7 @@ sealed interface TimelineItem {
val content: TimelineItemEventContent,
val sentTimeMillis: Long = 0L,
val sentTime: String = "",
val sentDate: String = "",
val isMine: Boolean = false,
val isEditable: Boolean,
val canBeRepliedTo: Boolean,

View file

@ -33,7 +33,7 @@
<string name="screen_room_attachment_source_camera_video">"Spela in video"</string>
<string name="screen_room_attachment_source_files">"Bilaga"</string>
<string name="screen_room_attachment_source_gallery">"Foto- och videobibliotek"</string>
<string name="screen_room_attachment_source_location">"Plats"</string>
<string name="screen_room_attachment_source_location">"Dela plats"</string>
<string name="screen_room_attachment_source_poll">"Omröstning"</string>
<string name="screen_room_attachment_text_formatting">"Textformatering"</string>
<string name="screen_room_encrypted_history_banner">"Meddelandehistoriken är för närvarande otillgänglig."</string>

View file

@ -93,6 +93,7 @@ class DefaultMessagesEntryPointTest {
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()
override fun navigateToRoom(roomId: RoomId) = lambdaError()
override fun navigateToDeveloperSettings() = lambdaError()
}
val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID)
val params = MessagesEntryPoint.Params(initialTarget)

View file

@ -24,6 +24,8 @@ class FakeMessagesNavigator(
private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() },
private val onPreviewAttachmentLambda: (attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
private val onNavigateToRoomLambda: (roomId: RoomId, threadId: EventId?, serverNames: List<String>) -> Unit = { _, _, _ -> lambdaError() },
private val navigateToMemberLambda: (userId: UserId) -> Unit = { lambdaError() },
private val navigateToDeveloperSettingsLambda: () -> Unit = { lambdaError() },
private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() },
private val closeLambda: () -> Unit = { lambdaError() },
) : MessagesNavigator {
@ -51,10 +53,18 @@ class FakeMessagesNavigator(
onNavigateToRoomLambda(roomId, eventId, serverNames)
}
override fun navigateToMember(userId: UserId) {
navigateToMemberLambda(userId)
}
override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) {
onOpenThreadLambda(threadRootId, focusedEventId)
}
override fun navigateToDeveloperSettings() {
navigateToDeveloperSettingsLambda()
}
override fun close() {
closeLambda()
}

View file

@ -0,0 +1,323 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.impl.messagecomposer
import android.net.Uri
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.LocationService
import io.element.android.features.location.test.FakeLocationService
import io.element.android.features.messages.impl.FakeMessagesNavigator
import io.element.android.features.messages.impl.MessagesNavigator
import io.element.android.features.messages.impl.draft.ComposerDraftService
import io.element.android.features.messages.impl.draft.FakeComposerDraftService
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
import io.element.android.libraries.slashcommands.api.SlashCommand
import io.element.android.libraries.slashcommands.api.SlashCommandService
import io.element.android.libraries.slashcommands.test.FakeSlashCommandService
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class MessageComposerPresenterSlashCommandTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val pickerProvider = FakePickerProvider().apply {
givenResult(mockk()) // Uri is not available in JVM, so the only way to have a non-null Uri is using Mockk
}
private val mediaPreProcessor = FakeMediaPreProcessor()
private val snackbarDispatcher = SnackbarDispatcher()
private val mockMediaUrl: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
private val analyticsService = FakeAnalyticsService()
private val notificationConversationService = FakeNotificationConversationService()
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
presenter.test {
val initialState = awaitFirstItem()
assertThat(initialState.isFullScreen).isFalse()
assertThat(initialState.textEditorState.messageHtml()).isEmpty()
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
assertThat(initialState.showAttachmentSourcePicker).isFalse()
assertThat(initialState.canShareLocation).isTrue()
}
}
@Test
fun `present - slash command error sets failure`() = runTest {
val presenter = createPresenter(
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.ErrorUnknownSlashCommand(A_FAILURE_REASON) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
val errorState = awaitItem()
assertThat(errorState.slashCommandAction.isFailure()).isTrue()
assertThat(errorState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON)
// Composer should not be reset when command is an error
assertThat(errorState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
// Close the error
errorState.eventSink(MessageComposerEvent.ClearSlashError)
val finalState = awaitItem()
assertThat(finalState.slashCommandAction.isUninitialized()).isTrue()
}
}
@Test
fun `present - slash command navigation ShowUser navigates to member and resets composer`() = runTest {
val navigateToMember = lambdaRecorder<UserId, Unit> {}
val navigator = FakeMessagesNavigator(navigateToMemberLambda = navigateToMember)
val presenter = createPresenter(
navigator = navigator,
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.ShowUser(A_USER_ID) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
advanceUntilIdle()
// navigation should be invoked and composer reset
navigateToMember.assertions().isCalledOnce().with(value(A_USER_ID))
assertThat(initialState.textEditorState.messageHtml()).isEmpty()
}
}
@Test
fun `present - slash command navigation DevTools navigates to developer settings and resets composer`() = runTest {
val navigateToDev = lambdaRecorder<Unit> { }
val navigator = FakeMessagesNavigator(navigateToDeveloperSettingsLambda = navigateToDev)
val presenter = createPresenter(
navigator = navigator,
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.DevTools }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
advanceUntilIdle()
navigateToDev.assertions().isCalledOnce()
assertThat(initialState.textEditorState.messageHtml()).isEmpty()
}
}
@Test
fun `present - slash command send message proceeds and resets composer`() = runTest {
val presenter = createPresenter(
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.SendPlainText(A_MESSAGE) },
proceedSendMessageResult = { _, _ -> Result.success(Unit) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
advanceUntilIdle()
// Composer reset after successful slash send
assertThat(initialState.textEditorState.messageHtml()).isEmpty()
// Ensure no failure
assertThat(initialState.slashCommandAction.isFailure()).isFalse()
}
}
@Test
fun `present - slash command send message failure sets failure state`() = runTest {
val presenter = createPresenter(
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.SendPlainText("A_MESSAGE") },
proceedSendMessageResult = { _, _ -> Result.failure(Exception(A_FAILURE_REASON)) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
val failureState = awaitItem()
assertThat(failureState.slashCommandAction.isFailure()).isTrue()
assertThat(failureState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON)
// Clear the error
failureState.eventSink(MessageComposerEvent.ClearSlashError)
val finalState = awaitItem()
assertThat(finalState.slashCommandAction.isUninitialized()).isTrue()
}
}
@Test
fun `present - slash command admin proceeds and resets state on success`() = runTest {
val presenter = createPresenter(
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.BanUser(A_USER_ID, null) },
proceedAdminResult = { _ -> Result.success(Unit) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
val loadingState = awaitItem()
assertThat(loadingState.slashCommandAction.isLoading()).isTrue()
val successState = awaitItem()
// After success, state should be Uninitialized
assertThat(successState.slashCommandAction.isUninitialized()).isTrue()
assertThat(successState.textEditorState.messageHtml()).isEmpty()
}
}
@Test
fun `present - slash command admin proceeds and emit failure on error`() = runTest {
val presenter = createPresenter(
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.BanUser(A_USER_ID, null) },
proceedAdminResult = { _ -> Result.failure(Exception(A_FAILURE_REASON)) }
)
)
presenter.test {
val initialState = awaitFirstItem()
initialState.textEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvent.SendMessage)
val loadingState = awaitItem()
assertThat(loadingState.slashCommandAction.isLoading()).isTrue()
val failureState = awaitItem()
assertThat(failureState.slashCommandAction.isFailure()).isTrue()
assertThat(failureState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON)
// Clear error
failureState.eventSink(MessageComposerEvent.ClearSlashError)
val finalState = awaitItem()
assertThat(finalState.slashCommandAction.isUninitialized()).isTrue()
}
}
private fun TestScope.createPresenter(
room: JoinedRoom = FakeJoinedRoom(
typingNoticeResult = { Result.success(Unit) }
),
timeline: Timeline = room.liveTimeline,
navigator: MessagesNavigator = FakeMessagesNavigator(),
pickerProvider: PickerProvider = this@MessageComposerPresenterSlashCommandTest.pickerProvider,
locationService: LocationService = FakeLocationService(true),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
mediaPreProcessor: MediaPreProcessor = this@MessageComposerPresenterSlashCommandTest.mediaPreProcessor,
snackbarDispatcher: SnackbarDispatcher = this@MessageComposerPresenterSlashCommandTest.snackbarDispatcher,
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
permalinkParser: PermalinkParser = FakePermalinkParser(),
mentionSpanProvider: MentionSpanProvider = MentionSpanProvider(
permalinkParser = permalinkParser,
mentionSpanFormatter = FakeMentionSpanFormatter(),
mentionSpanTheme = MentionSpanTheme(A_USER_ID)
),
textPillificationHelper: TextPillificationHelper = FakeTextPillificationHelper(),
isRichTextEditorEnabled: Boolean = true,
draftService: ComposerDraftService = FakeComposerDraftService(),
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
isInThread: Boolean = false,
slashCommandService: SlashCommandService = FakeSlashCommandService(),
) = MessageComposerPresenter(
navigator = navigator,
sessionCoroutineScope = this,
isInThread = isInThread,
room = room,
mediaPickerProvider = pickerProvider,
sessionPreferencesStore = sessionPreferencesStore,
localMediaFactory = localMediaFactory,
mediaSenderFactory = MediaSenderFactory { timelineMode ->
DefaultMediaSender(
preProcessor = mediaPreProcessor,
room = room,
timelineMode = timelineMode,
mediaOptimizationConfigProvider = {
MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD
)
}
)
},
snackbarDispatcher = snackbarDispatcher,
analyticsService = analyticsService,
locationService = locationService,
messageComposerContext = DefaultMessageComposerContext(),
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(),
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
permalinkParser = permalinkParser,
permalinkBuilder = permalinkBuilder,
timelineController = TimelineController(room, timeline),
draftService = draftService,
mentionSpanProvider = mentionSpanProvider,
pillificationHelper = textPillificationHelper,
suggestionsProcessor = SuggestionsProcessor(slashCommandService = slashCommandService),
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
notificationConversationService = notificationConversationService,
slashCommandService = slashCommandService,
).apply {
isTesting = true
showTextFormatting = isRichTextEditorEnabled
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
}

View file

@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter
import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper
import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.EventId
@ -46,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.api.timeline.MsgType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
@ -89,6 +91,9 @@ import io.element.android.libraries.preferences.api.store.SessionPreferencesStor
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService
import io.element.android.libraries.slashcommands.api.SlashCommand
import io.element.android.libraries.slashcommands.api.SlashCommandService
import io.element.android.libraries.slashcommands.test.FakeSlashCommandService
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@ -144,6 +149,7 @@ class MessageComposerPresenterTest {
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal)
assertThat(initialState.showAttachmentSourcePicker).isFalse()
assertThat(initialState.canShareLocation).isTrue()
assertThat(initialState.slashCommandAction).isEqualTo(AsyncAction.Uninitialized)
}
}
@ -374,10 +380,13 @@ class MessageComposerPresenterTest {
val presenter = createPresenter(
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
sendMessageLambda = { _, _, _ -> Result.success(Unit) }
sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) }
},
typingNoticeResult = { Result.success(Unit) }
),
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.NotACommand }
),
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
@ -409,10 +418,13 @@ class MessageComposerPresenterTest {
isRichTextEditorEnabled = false,
room = FakeJoinedRoom(
liveTimeline = FakeTimeline().apply {
sendMessageLambda = { _, _, _ -> Result.success(Unit) }
sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) }
},
typingNoticeResult = { Result.success(Unit) }
),
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.NotACommand }
),
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
@ -602,7 +614,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - reply message`() = runTest {
val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<IntentionalMention>, _: Boolean, _: MsgType ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
@ -633,7 +645,7 @@ class MessageComposerPresenterTest {
assert(replyMessageLambda)
.isCalledOnce()
.with(any(), value(A_REPLY), value(A_REPLY), any(), value(false))
.with(any(), value(A_REPLY), value(A_REPLY), any(), value(false), value(MsgType.MSG_TYPE_TEXT))
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
@ -967,7 +979,12 @@ class MessageComposerPresenterTest {
)
givenRoomInfo(aRoomInfo(isDirect = false))
}
val presenter = createPresenter(room)
val presenter = createPresenter(
room = room,
slashCommandService = FakeSlashCommandService(
getSuggestionsResult = { _, _ -> emptyList() },
),
)
presenter.test {
val initialState = awaitItem()
@ -1086,13 +1103,13 @@ class MessageComposerPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - send messages with intentional mentions`() = runTest {
val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<IntentionalMention>, _: Boolean ->
val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List<IntentionalMention>, _: Boolean, _: MsgType ->
Result.success(Unit)
}
val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List<IntentionalMention> ->
Result.success(Unit)
}
val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention> ->
val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention>, _: MsgType, _: Boolean ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
@ -1104,7 +1121,12 @@ class MessageComposerPresenterTest {
liveTimeline = timeline,
typingNoticeResult = { Result.success(Unit) }
)
val presenter = createPresenter(room = room)
val presenter = createPresenter(
room = room,
slashCommandService = FakeSlashCommandService(
parseResult = { _, _, _ -> SlashCommand.NotACommand }
),
)
presenter.test {
val initialState = awaitFirstItem()
@ -1122,7 +1144,7 @@ class MessageComposerPresenterTest {
advanceUntilIdle()
sendMessageResult.assertions().isCalledOnce()
.with(value(A_MESSAGE), any(), value(listOf(IntentionalMention.User(A_USER_ID))))
.with(value(A_MESSAGE), any(), value(listOf(IntentionalMention.User(A_USER_ID))), value(MsgType.MSG_TYPE_TEXT), value(false))
// Check intentional mentions on reply sent
initialState.eventSink(MessageComposerEvent.SetMode(aReplyMode()))
@ -1139,7 +1161,7 @@ class MessageComposerPresenterTest {
assert(replyMessageLambda)
.isCalledOnce()
.with(any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_2))), value(false))
.with(any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_2))), value(false), value(MsgType.MSG_TYPE_TEXT))
// Check intentional mentions on edit message
skipItems(1)
@ -1512,9 +1534,12 @@ class MessageComposerPresenterTest {
isRichTextEditorEnabled: Boolean = true,
draftService: ComposerDraftService = FakeComposerDraftService(),
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
isInThread: Boolean = false,
slashCommandService: SlashCommandService = FakeSlashCommandService(),
) = MessageComposerPresenter(
navigator = navigator,
sessionCoroutineScope = this,
isInThread = isInThread,
room = room,
mediaPickerProvider = pickerProvider,
sessionPreferencesStore = sessionPreferencesStore,
@ -1545,9 +1570,10 @@ class MessageComposerPresenterTest {
draftService = draftService,
mentionSpanProvider = mentionSpanProvider,
pillificationHelper = textPillificationHelper,
suggestionsProcessor = SuggestionsProcessor(),
suggestionsProcessor = SuggestionsProcessor(slashCommandService = slashCommandService),
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
notificationConversationService = notificationConversationService,
slashCommandService = slashCommandService,
).apply {
isTesting = true
showTextFormatting = isRichTextEditorEnabled

View file

@ -17,6 +17,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
import io.element.android.libraries.slashcommands.test.FakeSlashCommandService
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.SuggestionType
@ -27,10 +29,13 @@ import org.junit.Test
class SuggestionsProcessorTest {
private fun aMentionSuggestion(text: String) = Suggestion(0, 1, SuggestionType.Mention, text)
private fun aRoomSuggestion(text: String) = Suggestion(0, 1, SuggestionType.Room, text)
private val aCommandSuggestion = Suggestion(0, 1, SuggestionType.Command, "")
private val aCustomSuggestion = Suggestion(0, 1, SuggestionType.Custom("*"), "")
private val suggestionsProcessor = SuggestionsProcessor()
private val suggestionsProcessor = SuggestionsProcessor(
slashCommandService = FakeSlashCommandService(
getSuggestionsResult = { _, _ -> emptyList() },
),
)
@Test
fun `processing null suggestion will return empty suggestion`() = runTest {
@ -40,18 +45,59 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@Test
fun `processing Command will return empty suggestion`() = runTest {
val result = suggestionsProcessor.process(
suggestion = aCommandSuggestion,
fun `processing Command will return suggestions from the slash service`() = runTest {
val suggestionsProcessorWithCommand = SuggestionsProcessor(
slashCommandService = FakeSlashCommandService(
getSuggestionsResult = { _, _ ->
listOf(
SlashCommandSuggestion(
command = "aCommand",
parameters = null,
description = "A description",
),
)
},
),
)
val result = suggestionsProcessorWithCommand.process(
suggestion = Suggestion(0, 1, SuggestionType.Command, ""),
roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())),
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isNotEmpty()
}
@Test
fun `processing Command will return empty list if start of suggestion is not 0`() = runTest {
val suggestionsProcessorWithCommand = SuggestionsProcessor(
slashCommandService = FakeSlashCommandService(
getSuggestionsResult = { _, _ ->
listOf(
SlashCommandSuggestion(
command = "aCommand",
parameters = null,
description = "A description",
),
)
},
),
)
val result = suggestionsProcessorWithCommand.process(
suggestion = Suggestion(1, 2, SuggestionType.Command, ""),
roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())),
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -64,6 +110,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -76,6 +123,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -88,6 +136,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -100,6 +149,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -120,6 +170,7 @@ class SuggestionsProcessorTest {
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -149,6 +200,7 @@ class SuggestionsProcessorTest {
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -178,6 +230,7 @@ class SuggestionsProcessorTest {
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -198,6 +251,7 @@ class SuggestionsProcessorTest {
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -227,6 +281,7 @@ class SuggestionsProcessorTest {
),
currentUserId = A_USER_ID,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -240,6 +295,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -257,6 +313,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = UserId("@alice:server.org"),
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -270,6 +327,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -283,6 +341,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEmpty()
}
@ -296,6 +355,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -313,6 +373,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { true },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(
@ -331,6 +392,7 @@ class SuggestionsProcessorTest {
roomAliasSuggestions = emptyList(),
currentUserId = A_USER_ID_2,
canSendRoomMention = { false },
isInThread = false,
)
assertThat(result).isEqualTo(
listOf(

View file

@ -9,6 +9,7 @@
package io.element.android.features.messages.impl.pinned.list
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator {
@ -26,4 +27,9 @@ class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator {
override fun forwardEvent(eventId: EventId) {
onForwardEventClickLambda?.invoke(eventId)
}
var onOpenThreadLambda: ((ThreadId) -> Unit)? = null
override fun navigateToThread(threadRootId: ThreadId) {
onOpenThreadLambda?.invoke(threadRootId)
}
}

View file

@ -12,6 +12,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.MsgType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_UNIQUE_ID
@ -154,10 +155,10 @@ class TimelineControllerTest {
@Test
fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest {
val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention> ->
val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List<IntentionalMention>, _: MsgType, _: Boolean ->
Result.success(Unit)
}
val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List<IntentionalMention> ->
val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List<IntentionalMention>, _: MsgType, _: Boolean ->
Result.success(Unit)
}
val liveTimeline = FakeTimeline(name = "live").apply {

View file

@ -26,5 +26,5 @@ dependencies {
implementation(projects.libraries.voicerecorder.test)
implementation(projects.services.analytics.test)
implementation(projects.tests.testutils)
implementation(projects.libraries.mediaupload.impl)
implementation(projects.libraries.mediaupload.api)
}

View file

@ -12,13 +12,10 @@ import io.element.android.features.messages.impl.voicemessages.composer.DefaultV
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.impl.DefaultMediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaupload.test.FakeMediaSender
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
import io.element.android.services.analytics.test.FakeAnalyticsService
@ -26,12 +23,7 @@ import kotlinx.coroutines.CoroutineScope
class FakeDefaultVoiceMessageComposerPresenterFactory(
private val sessionCoroutineScope: CoroutineScope,
private val mediaSender: MediaSender = DefaultMediaSender(
preProcessor = FakeMediaPreProcessor(),
room = FakeJoinedRoom(),
timelineMode = Timeline.Mode.Live,
mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
),
private val mediaSender: MediaSender = FakeMediaSender(),
) : DefaultVoiceMessageComposerPresenter.Factory {
override fun create(timelineMode: Timeline.Mode): DefaultVoiceMessageComposerPresenter {
return DefaultVoiceMessageComposerPresenter(

View file

@ -24,7 +24,7 @@ dependencies {
implementation(projects.features.migration.api)
implementation(projects.libraries.architecture)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.preferences.impl)
implementation(projects.libraries.preferences.api)
implementation(libs.androidx.datastore.preferences)
implementation(projects.features.rageshake.api)
implementation(projects.libraries.designsystem)

View file

@ -28,6 +28,9 @@ interface PreferencesEntryPoint : FeatureEntryPoint {
@Parcelize
data object NotificationTroubleshoot : InitialTarget
@Parcelize
data object DeveloperSettings : InitialTarget
}
data class Params(val initialElement: InitialTarget) : NodeInputs

View file

@ -34,4 +34,5 @@ internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) {
is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root
is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings
PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot -> PreferencesFlowNode.NavTarget.TroubleshootNotifications
PreferencesEntryPoint.InitialTarget.DeveloperSettings -> PreferencesFlowNode.NavTarget.DeveloperSettings
}

View file

@ -192,7 +192,11 @@ class PreferencesFlowNode(
}
override fun onDone() {
backstack.pop()
if (backstack.canPop()) {
backstack.pop()
} else {
navigateUp()
}
}
}
createNode<DeveloperSettingsNode>(buildContext, listOf(developerSettingsCallback))

View file

@ -53,6 +53,9 @@ import io.element.android.libraries.matrix.ui.components.AvatarPickerView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
/**
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3182-36115&t=U1vS3px9HzlzWYd7-4
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditUserProfileView(
@ -125,7 +128,7 @@ fun EditUserProfileView(
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(40.dp))
Spacer(modifier = Modifier.height(32.dp))
TextField(
label = stringResource(R.string.screen_edit_profile_display_name),
value = state.displayName,

View file

@ -2,6 +2,6 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="crash_detection_dialog_content">"%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?"</string>
<string name="rageshake_detection_dialog_content">"Atrodo, kad nusivylęs purtote telefoną. Ar norėtumėte atidaryti pranešimo apie klaidas ekraną?"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake">"Gerai pakratyti"</string>
<string name="settings_rageshake_detection_threshold">"Aptikimo riba"</string>
</resources>

View file

@ -10,6 +10,6 @@
<string name="screen_bug_report_include_crash_logs">"Siųsti gedimų žurnalus"</string>
<string name="screen_bug_report_include_logs">"Leisti žurnalus"</string>
<string name="screen_bug_report_include_screenshot">"Siųsti ekrano nuotrauką"</string>
<string name="screen_bug_report_logs_description">"Prie žinutės bus pridėti žurnalai, kad įsitikintumėme, jog viskas veikia tinkamai. Jei norite išsiųsti savo žinutę be žurnalų, išjunkite šį nustatymą."</string>
<string name="screen_bug_report_logs_description">"Žurnalai bus įtraukti į jūsų žinutę, kad būtų užtikrinta, jog viskas veikia tinkamai. Kad išsiųstumėte žinutę be žurnalų, išjunkite šį nustatymą."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?"</string>
</resources>

View file

@ -39,6 +39,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun navigateToGlobalNotificationSettings()
fun navigateToDeveloperSettings()
fun navigateToRoom(roomId: RoomId, serverNames: List<String>)
fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean)
fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean)

View file

@ -388,6 +388,10 @@ class RoomDetailsFlowNode(
override fun navigateToRoom(roomId: RoomId) {
callback.navigateToRoom(roomId, emptyList())
}
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
}
return messagesEntryPoint.createNode(
parentNode = this,

View file

@ -69,6 +69,7 @@ class DefaultRoomDetailsEntryPointTest {
}
val callback = object : RoomDetailsEntryPoint.Callback {
override fun navigateToGlobalNotificationSettings() = lambdaError()
override fun navigateToDeveloperSettings() = lambdaError()
override fun navigateToRoom(roomId: RoomId, serverNames: List<String>) = lambdaError()
override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError()
override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError()

View file

@ -51,6 +51,12 @@ import io.element.android.libraries.matrix.ui.components.AvatarPickerView
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
/**
* For space:
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2216-110711
* For room:
* https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3187-47342
*/
@Composable
fun RoomDetailsEditView(
state: RoomDetailsEditState,
@ -102,11 +108,11 @@ fun RoomDetailsEditView(
) {
Spacer(modifier = Modifier.height(24.dp))
val avatarPickerState = remember(state.roomAvatarUrl, state.roomRawName) {
val size = AvatarSize.EditRoomDetails
val size = if (state.isSpace) AvatarSize.EditSpaceDetails else AvatarSize.EditRoomDetails
val type = if (state.isSpace) AvatarType.Space() else AvatarType.Room()
AvatarPickerState.Selected(
avatarData = AvatarData(id = state.roomId.value, name = state.roomRawName, size = size, url = state.roomAvatarUrl),
type = type
type = type,
)
}
AvatarPickerView(

View file

@ -2,16 +2,17 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_chat_backup_key_backup_action_disable">"Vypnout zálohování"</string>
<string name="screen_chat_backup_key_backup_action_enable">"Zapnout zálohování"</string>
<string name="screen_chat_backup_key_backup_description">"Bezpečně uložte svou kryptografickou identitu a klíče zpráv na serveru. To vám umožní zobrazit historii zpráv na všech nových zařízeních. %1$s."</string>
<string name="screen_chat_backup_key_backup_description">"To vám umožní zobrazit historii chatu na všech nových zařízeních a je to nutné pro zálohování chatů a digitální identity. %1$s ."</string>
<string name="screen_chat_backup_key_backup_title">"Úložiště klíčů"</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Pro nastavení obnovení musí být zapnuto úložiště klíčů."</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Pro zálohování chatů musí být zapnuto ukládání klíčů."</string>
<string name="screen_chat_backup_key_storage_toggle_description">"Nahrát klíče z tohoto zařízení"</string>
<string name="screen_chat_backup_key_storage_toggle_title">"Povolit ukládání klíčů"</string>
<string name="screen_chat_backup_recovery_action_change">"Změnit klíč pro obnovení"</string>
<string name="screen_chat_backup_recovery_action_change_description">"Obnovte svou kryptografickou identitu a historii zpráv pomocí klíče pro obnovení, pokud jste ztratili všechna stávající zařízení."</string>
<string name="screen_chat_backup_recovery_action_change_description">"Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení."</string>
<string name="screen_chat_backup_recovery_action_confirm">"Zadejte klíč pro obnovení"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Vaše úložiště klíčů je momentálně nesynchronizované."</string>
<string name="screen_chat_backup_recovery_action_setup">"Nastavení obnovy"</string>
<string name="screen_chat_backup_recovery_action_setup">"Získat klíč pro obnovení"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Otevřít %1$s na stolním počítači"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Znovu se přihlaste ke svému účtu"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Když budete vyzváni k ověření vašeho zařízení, vyberte %1$s"</string>
@ -23,12 +24,12 @@
<string name="screen_encryption_reset_bullet_1">"Podrobnosti o vašem účtu, kontaktech, preferencích a seznamu chatu budou zachovány"</string>
<string name="screen_encryption_reset_bullet_2">"Ztratíte svou stávající historii zpráv"</string>
<string name="screen_encryption_reset_bullet_3">"Budete muset znovu ověřit všechna stávající zařízení a kontakty"</string>
<string name="screen_encryption_reset_footer">"Obnovte svou identitu pouze v případě, že nemáte přístup k jinému přihlášenému zařízení a ztratili jste klíč pro obnovení."</string>
<string name="screen_encryption_reset_title">"Obnovte svou identitu v případě, že nemůžete potvrdit jiným způsobem"</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Vypnout"</string>
<string name="screen_key_backup_disable_confirmation_description">"Pokud se odhlásíte ze všech zařízení, přijdete o zašifrované zprávy."</string>
<string name="screen_key_backup_disable_confirmation_title">"Opravdu chcete vypnout zálohování?"</string>
<string name="screen_key_backup_disable_description">"Vypnutím zálohování odstraníte zálohu aktuálního šifrovacího klíče a vypnete další bezpečnostní funkce. V tomto případě budete:"</string>
<string name="screen_encryption_reset_footer">"Digitální identitu resetujte pouze v případě, že nemáte přístup k jinému ověřenému zařízení a nemáte klíč pro obnovení."</string>
<string name="screen_encryption_reset_title">"Nelze potvrdit? Budete muset resetovat svou digitální identitu."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Smazat"</string>
<string name="screen_key_backup_disable_confirmation_description">"Pokud odeberete všechna zařízení, ztratíte svou šifrovanou historii chatu a budete muset resetovat svou digitální identitu."</string>
<string name="screen_key_backup_disable_confirmation_title">"Opravdu chcete smazat úložiště klíčů?"</string>
<string name="screen_key_backup_disable_description">"Smazáním úložiště klíčů odstraníte ze serveru klíče digitální identity a zpráv a vypnete následující bezpečnostní funkce:"</string>
<string name="screen_key_backup_disable_description_point_1">"Nemít v nových zařízeních šifrovanou historii zpráv"</string>
<string name="screen_key_backup_disable_description_point_2">"Ztratíte přístup k šifrovaným zprávám, pokud jste všude odhlášeni z %1$s"</string>
<string name="screen_key_backup_disable_title">"Opravdu chcete vypnout zálohování?"</string>
@ -58,12 +59,12 @@
<string name="screen_recovery_key_setup_generate_key">"Vygenerovat klíč pro obnovení"</string>
<string name="screen_recovery_key_setup_generate_key_description">"Toto s nikým nesdílejte!"</string>
<string name="screen_recovery_key_setup_success">"Nastavení obnovení bylo úspěšné"</string>
<string name="screen_recovery_key_setup_title">"Nastavení obnovy"</string>
<string name="screen_recovery_key_setup_title">"Získat klíč pro obnovení"</string>
<string name="screen_reset_encryption_confirmation_alert_action">"Ano, resetovat nyní"</string>
<string name="screen_reset_encryption_confirmation_alert_subtitle">"Tento proces je nevratný."</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Opravdu chcete obnovit svou identitu?"</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Opravdu chcete resetovat svou digitální identitu?"</string>
<string name="screen_reset_encryption_password_error">"Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu."</string>
<string name="screen_reset_encryption_password_placeholder">"Zadejte…"</string>
<string name="screen_reset_encryption_password_subtitle">"Potvrďte, že chcete obnovit svou identitu."</string>
<string name="screen_reset_encryption_password_subtitle">"Potvrďte, že chcete resetovat svou digitální identitu."</string>
<string name="screen_reset_encryption_password_title">"Pro pokračování zadejte heslo k účtu"</string>
</resources>

View file

@ -2,16 +2,17 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_chat_backup_key_backup_action_disable">"Slet nøglelager"</string>
<string name="screen_chat_backup_key_backup_action_enable">"Aktivér sikkerhedskopiering"</string>
<string name="screen_chat_backup_key_backup_description">"Gem din kryptografiske identitet og meddelelsesnøgler sikkert på serveren. Dette giver dig mulighed for at se din meddelelseshistorik på alle nye enheder. %1$s."</string>
<string name="screen_chat_backup_key_backup_description">"Dette giver dig mulighed for at se din chathistorik på alle nye enheder og er påkrævet til sikkerhedskopiering af chats og digital identitet.%1$s ."</string>
<string name="screen_chat_backup_key_backup_title">"Nøgleopbevaring"</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Nøglelagring skal være slået til for at konfigurere gendannelse."</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Nøglelagring skal være slået til for at konfigurere gendannelse af dine samtaler."</string>
<string name="screen_chat_backup_key_storage_toggle_description">"Upload nøgler fra denne enhed"</string>
<string name="screen_chat_backup_key_storage_toggle_title">"Tillad lagring af nøgler"</string>
<string name="screen_chat_backup_recovery_action_change">"Skift gendannelsesnøgle"</string>
<string name="screen_chat_backup_recovery_action_change_description">"Gendan din kryptografiske identitet og beskedhistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder."</string>
<string name="screen_chat_backup_recovery_action_change_description">"Dine samtaler sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle."</string>
<string name="screen_chat_backup_recovery_action_confirm">"Indtast gendannelsesnøgle"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Din nøglelagring er i øjeblikket ikke synkroniseret."</string>
<string name="screen_chat_backup_recovery_action_setup">"Opsæt gendannelse"</string>
<string name="screen_chat_backup_recovery_action_setup">"Hent gendannelsesnøgle"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Dine chats sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Åbn %1$s på en stationær enhed"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Log ind på din konto igen"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Når du bliver bedt om at verificere din enhed, skal du vælge %1$s"</string>
@ -23,12 +24,12 @@
<string name="screen_encryption_reset_bullet_1">"Dine kontodetaljer, kontakter, personlige indstilliger og samtaler vil blive gemt"</string>
<string name="screen_encryption_reset_bullet_2">"Du mister al beskedhistorik, der kun er gemt på serveren."</string>
<string name="screen_encryption_reset_bullet_3">"Du bliver nødt til at verificere alle dine eksisterende enheder og kontakter påny"</string>
<string name="screen_encryption_reset_footer">"Nulstil kun din identitet, hvis du ikke har adgang til en anden enhed, der er logget ind, og du har mistet din gendannelsesnøgle."</string>
<string name="screen_encryption_reset_title">"Kan du ikke bekræfte? Du skal nulstille din identitet."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Slå fra"</string>
<string name="screen_key_backup_disable_confirmation_description">"Du mister dine krypterede meddelelser, hvis du er logget ud af alle enheder."</string>
<string name="screen_key_backup_disable_confirmation_title">"Er du sikker på, at du vil slå sikkerhedskopiering fra?"</string>
<string name="screen_key_backup_disable_description">"Hvis du sletter nøglelageret, fjernes din kryptografiske identitet og meddelelsesnøgler fra serveren og følgende sikkerhedsfunktioner deaktiveres:"</string>
<string name="screen_encryption_reset_footer">"Nulstil kun din digitale identitet, hvis du ikke har adgang til en anden enhed, der er logget ind, og du har mistet din gendannelsesnøgle."</string>
<string name="screen_encryption_reset_title">"Kan du ikke bekræfte? Du er nødt til at nulstille din digitale identitet."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Slet"</string>
<string name="screen_key_backup_disable_confirmation_description">"Du mister din krypterede chathistorik og skal nulstille din digitale identitet, hvis du fjerner alle dine enheder."</string>
<string name="screen_key_backup_disable_confirmation_title">"Er du sikker på, at du vil slette nøglelageret?"</string>
<string name="screen_key_backup_disable_description">"Hvis du sletter nøglelageret, fjernes din kryptografiske identitet og beskednøgler fra serveren og følgende sikkerhedsfunktioner deaktiveres:"</string>
<string name="screen_key_backup_disable_description_point_1">"Du vil ikke kunne se historikken for krypterede beskeder på nye enheder"</string>
<string name="screen_key_backup_disable_description_point_2">"Du mister adgangen til dine krypterede meddelelser, hvis du er logget ud %1$s overalt"</string>
<string name="screen_key_backup_disable_title">"Er du sikker på, at du vil deaktivere nøglelagring og slette lageret?"</string>
@ -58,12 +59,12 @@
<string name="screen_recovery_key_setup_generate_key">"Generer din gendannelsesnøgle"</string>
<string name="screen_recovery_key_setup_generate_key_description">"Del ikke dette med nogen!"</string>
<string name="screen_recovery_key_setup_success">"Opsætning af gendannelse lykkedes"</string>
<string name="screen_recovery_key_setup_title">"Opsæt gendannelse"</string>
<string name="screen_recovery_key_setup_title">"Hent gendannelsesnøgle"</string>
<string name="screen_reset_encryption_confirmation_alert_action">"Ja, nulstil nu"</string>
<string name="screen_reset_encryption_confirmation_alert_subtitle">"Denne proces er irreversibel."</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Er du sikker på, at du ønsker at nulstille din identitet?"</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Er du sikker på, at du vil nulstille din digitale identitet?"</string>
<string name="screen_reset_encryption_password_error">"Der opstod en ukendt fejl. Kontroller, at adgangskoden til din konto er korrekt, og prøv igen."</string>
<string name="screen_reset_encryption_password_placeholder">"Indtast…"</string>
<string name="screen_reset_encryption_password_subtitle">"Bekræft, at du ønsker at nulstille din identitet."</string>
<string name="screen_reset_encryption_password_subtitle">"Bekræft, at du ønsker at nulstille din digitale identitet."</string>
<string name="screen_reset_encryption_password_title">"Indtast adgangskoden til din konto for at fortsætte"</string>
</resources>

View file

@ -12,6 +12,7 @@
<string name="screen_chat_backup_recovery_action_confirm">"Anna palautusavain"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Avainten säilytys ei ole tällä hetkellä synkronoitu."</string>
<string name="screen_chat_backup_recovery_action_setup">"Hanki palautusavain"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Keskustelusi varmuuskopioidaan automaattisesti päästä päähän -salauksella. Jotta voit palauttaa tämän varmuuskopion ja säilyttää digitaalisen identiteettisi, kun menetät pääsyn kaikkiin laitteisiisi, tarvitset palautusavaimesi."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Avaa %1$s tietokoneella"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Kirjaudu tilillesi uudelleen"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Kun sinua pyydetään vahvistamaan laitteesi, valitse %1$s"</string>

View file

@ -12,6 +12,7 @@
<string name="screen_chat_backup_recovery_action_confirm">"Adja meg a helyreállítási kulcsot"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"A kulcstároló jelenleg nincs szinkronizálva."</string>
<string name="screen_chat_backup_recovery_action_setup">"Helyreállítási kulcs beszerzése"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"A csevegésekről automatikusan készül biztonsági mentés végpontok közötti titkosítással. A biztonsági mentés helyreállításához és digitális személyazonossága megőrzéséhez szüksége lesz a helyreállítási kulcsára, ha elveszíti a hozzáférést az összes eszközéhez."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Jelentkezzen be újra a fiókjába"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Amikor az eszköz ellenőrzését kéri, válassza ezt a lehetőséget: %1$s"</string>
@ -23,11 +24,11 @@
<string name="screen_encryption_reset_bullet_1">"A fiókadatok, a kapcsolatok, a beállítások és a csevegéslista megmarad"</string>
<string name="screen_encryption_reset_bullet_2">"Elveszíti meglévő üzenetelőzményeit"</string>
<string name="screen_encryption_reset_bullet_3">"Újból ellenőriznie kell az összes meglévő eszközét és csevegőpartnerét"</string>
<string name="screen_encryption_reset_footer">"Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá másik bejelentkezett eszközhöz, és elvesztette a helyreállítási kulcsot."</string>
<string name="screen_encryption_reset_footer">"Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá más bejelentkezett eszközhöz, és elveszítette a helyreállítási kulcsát."</string>
<string name="screen_encryption_reset_title">"Nem tudja megerősíteni? Alaphelyzetbe kell állítania a digitális személyazonosságát."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Kikapcsolás"</string>
<string name="screen_key_backup_disable_confirmation_description">"Ha kijelentkezik az összes eszközéről, akkor elveszti a titkosított üzeneteit."</string>
<string name="screen_key_backup_disable_confirmation_title">"Biztos, hogy kikapcsolja a biztonsági mentéseket?"</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Törlés"</string>
<string name="screen_key_backup_disable_confirmation_description">"Ha eltávolítja az összes eszközét, elveszíti titkosított csevegési előzményeit, és újra be kell állítania digitális személyazonosságát."</string>
<string name="screen_key_backup_disable_confirmation_title">"Biztosan törölni szeretné a kulcstárolót?"</string>
<string name="screen_key_backup_disable_description">"A kulcstároló törlése eltávolítja a digitális személyazonosságát és az üzenetkulcsait a kiszolgálóról, és kikapcsolja a következő biztonsági funkciókat:"</string>
<string name="screen_key_backup_disable_description_point_1">"Nem lesznek meg a titkosított üzenetek előzményei az új eszközein"</string>
<string name="screen_key_backup_disable_description_point_2">"Elveszti a hozzáférését a titkosított üzeneteihez, ha mindenhol kilép az %1$sből"</string>

View file

@ -4,14 +4,15 @@
<string name="screen_chat_backup_key_backup_action_enable">"백업 활성화"</string>
<string name="screen_chat_backup_key_backup_description">"이 설정을 통해 새로운 기기에서도 대화 기록을 확인할 수 있으며, 대화 및 디지털 신원 백업을 위해 반드시 필요합니다. %1$s."</string>
<string name="screen_chat_backup_key_backup_title">"키 저장소"</string>
<string name="screen_chat_backup_key_storage_disabled_error">"복구 설정을 하려면 키 저장을 켜야 합니다."</string>
<string name="screen_chat_backup_key_storage_disabled_error">"대화 내용을 백업하려면 키 저장소를 켜야 합니다."</string>
<string name="screen_chat_backup_key_storage_toggle_description">"이 장치에서 키 업로드"</string>
<string name="screen_chat_backup_key_storage_toggle_title">"키 저장 허용"</string>
<string name="screen_chat_backup_recovery_action_change">"복구 키 변경"</string>
<string name="screen_chat_backup_recovery_action_change_description">"기존의 모든 기기를 분실한 경우, 복구 키를 사용하여 암호화 ID와 메시지 기록을 복구할 수 있습니다."</string>
<string name="screen_chat_backup_recovery_action_change_description">"대화 내용은 종단간 암호화 기술로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다."</string>
<string name="screen_chat_backup_recovery_action_confirm">"복구 키를 입력하세요"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"현재 키 저장소가 동기화되지 않았습니다."</string>
<string name="screen_chat_backup_recovery_action_setup">"복구 키 가져오기"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"대화 내용은 종단간 암호화로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다."</string>
<string name="screen_create_new_recovery_key_list_item_1">"데스크톱 장치에서 %1$s 을 엽니다."</string>
<string name="screen_create_new_recovery_key_list_item_2">"계정에 다시 로그인하세요"</string>
<string name="screen_create_new_recovery_key_list_item_3">"장치를 확인하라는 메시지가 표시되면, %1$s 을 선택하세요"</string>

View file

@ -12,6 +12,7 @@
<string name="screen_chat_backup_recovery_action_confirm">"Введите ключ восстановления"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"В настоящее время резервная копия ваших чатов не синхронизирована."</string>
<string name="screen_chat_backup_recovery_action_setup">"Получить ключ восстановления"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления."</string>
<string name="screen_create_new_recovery_key_list_item_1">"Откройте %1$s на компьютере"</string>
<string name="screen_create_new_recovery_key_list_item_2">"Войдите в свой аккаунт еще раз"</string>
<string name="screen_create_new_recovery_key_list_item_3">"Когда потребуется подтвердить устройство, выберите %1$s"</string>
@ -25,9 +26,9 @@
<string name="screen_encryption_reset_bullet_3">"Вам нужно будет заново подтвердить все существующие устройства и контакты."</string>
<string name="screen_encryption_reset_footer">"Сбрасывайте личность только в том случае, если у вас нет доступа к другим устройству, на которых выполнен вход, и вы потеряли ключ восстановления."</string>
<string name="screen_encryption_reset_title">"Не можете подтвердить? Вам потребуется сбросить личность вашей учетной записи."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Выключить"</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Удалить"</string>
<string name="screen_key_backup_disable_confirmation_description">"Вы потеряете зашифрованные сообщения, если выйдете из всех устройств."</string>
<string name="screen_key_backup_disable_confirmation_title">"Вы действительно хотите отключить резервное копирование?"</string>
<string name="screen_key_backup_disable_confirmation_title">"Вы уверены, что хотите удалить хранилище ключей?"</string>
<string name="screen_key_backup_disable_description">"Удаление хранилища ключей приведёт к удалению вашей криптографической личности и ключей сообщений с сервера, а также отключению следующих функций безопасности:"</string>
<string name="screen_key_backup_disable_description_point_1">"Нет зашифрованной истории сообщений на новых устройствах"</string>
<string name="screen_key_backup_disable_description_point_2">"Вы потеряете доступ к зашифрованным сообщениям, если выйдете из %1$s везде"</string>

View file

@ -78,9 +78,6 @@ class SecurityAndPrivacyPresenter(
val isKnockEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false)
val isSpaceSettingsEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings)
}.collectAsState(false)
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val homeserverName = remember { matrixClient.userIdServerName() }
@ -248,7 +245,6 @@ class SecurityAndPrivacyPresenter(
saveAction = saveAction.value,
permissions = permissions,
isSpace = roomInfo.isSpace,
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
selectableJoinedSpaces = selectableJoinedSpaces,
spaceSelectionMode = spaceSelectionMode,
eventSink = ::handleEvent,

View file

@ -29,7 +29,6 @@ data class SecurityAndPrivacyState(
val homeserverName: String,
val showEnableEncryptionConfirmation: Boolean,
private val isKnockEnabled: Boolean,
private val isSpaceSettingsEnabled: Boolean,
val saveAction: AsyncAction<Unit>,
val isSpace: Boolean,
private val permissions: SecurityAndPrivacyPermissions,
@ -37,7 +36,7 @@ data class SecurityAndPrivacyState(
private val spaceSelectionMode: SpaceSelectionMode,
val eventSink: (SecurityAndPrivacyEvent) -> Unit
) {
val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None
val isSpaceMemberSelectable = spaceSelectionMode != SpaceSelectionMode.None
// Show SpaceMember option in two cases:
// - SpaceMember is the current saved value

View file

@ -138,7 +138,6 @@ fun aSecurityAndPrivacyState(
isSpace: Boolean = false,
selectableJoinedSpaces: Set<SpaceRoom> = emptySet(),
spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None,
isSpaceSettingsEnabled: Boolean = true,
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
) = SecurityAndPrivacyState(
editedSettings = editedSettings,
@ -151,6 +150,5 @@ fun aSecurityAndPrivacyState(
isSpace = isSpace,
selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(),
spaceSelectionMode = spaceSelectionMode,
isSpaceSettingsEnabled = isSpaceSettingsEnabled,
eventSink = eventSink,
)

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