Merge branch 'develop' into fix

This commit is contained in:
ganfra 2025-09-23 14:38:45 +02:00 committed by GitHub
commit 72e39c2abb
207 changed files with 2230 additions and 1541 deletions

View file

@ -9,6 +9,7 @@
<locale android:name="el"/> <locale android:name="el"/>
<locale android:name="en"/> <locale android:name="en"/>
<locale android:name="en_US"/> <locale android:name="en_US"/>
<locale android:name="eo"/>
<locale android:name="es"/> <locale android:name="es"/>
<locale android:name="et"/> <locale android:name="et"/>
<locale android:name="eu"/> <locale android:name="eu"/>

View file

@ -54,7 +54,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
@ -111,7 +110,7 @@ class RoomFlowNode(
data class JoinedRoom(val roomId: RoomId) : NavTarget data class JoinedRoom(val roomId: RoomId) : NavTarget
@Parcelize @Parcelize
data class Space(val spaceId: RoomId) : NavTarget data class JoinedSpace(val spaceId: RoomId) : NavTarget
} }
override fun onBuilt() { override fun onBuilt() {
@ -149,30 +148,28 @@ class RoomFlowNode(
.withPreviousValue() .withPreviousValue()
combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace -> combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace ->
Timber.d("Room membership: $membership") Timber.d("Room membership: $membership")
when (membership) { if (membership == CurrentUserMembership.JOINED) {
CurrentUserMembership.JOINED -> { if (isSpace) {
if (isSpace) { backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId))
backstack.newRoot(NavTarget.Space(spaceId = roomId)) } else {
} else { backstack.newRoot(NavTarget.JoinedRoom(roomId))
backstack.newRoot(NavTarget.JoinedRoom(roomId))
}
} }
else -> { } else {
if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) { val leavingFromCurrentDevice =
// The user left the room in this device, remove the room from the backstack membership == CurrentUserMembership.LEFT &&
if (!membershipUpdateFlow.first().isUserInRoom) { previousMembership == CurrentUserMembership.JOINED &&
navigateUp() membershipUpdateFlow.replayCache.lastOrNull()?.isUserInRoom == false
}
} else { if (leavingFromCurrentDevice) {
// Was invited or the room is not known, display the join room screen navigateUp()
backstack.newRoot( } else {
NavTarget.JoinRoom( backstack.newRoot(
roomId = roomId, NavTarget.JoinRoom(
serverNames = serverNames, roomId = roomId,
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, serverNames = serverNames,
) trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
) )
} )
} }
} }
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
@ -214,7 +211,7 @@ class RoomFlowNode(
) )
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} }
is NavTarget.Space -> { is NavTarget.JoinedSpace -> {
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single() val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
spaceEntryPoint.nodeBuilder(this, buildContext) spaceEntryPoint.nodeBuilder(this, buildContext)
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId)) .inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId))

View file

@ -93,6 +93,8 @@ allprojects {
// Fix compilation warning for annotations // Fix compilation warning for annotations
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
freeCompilerArgs.add("-Xannotation-default-target=first-only") freeCompilerArgs.add("-Xannotation-default-target=first-only")
// Opt-in to context receivers
freeCompilerArgs.add("-Xcontext-parameters")
} }
} }
} }

View file

@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string> <string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string>
<string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string> <string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string>
<string name="screen_room_change_permissions_delete_messages">"Dileu negeseuon"</string> <string name="screen_room_change_permissions_delete_messages">"Tynnu negeseuon"</string>
<string name="screen_room_change_permissions_everyone">"Pawb"</string> <string name="screen_room_change_permissions_everyone">"Pawb"</string>
<string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string> <string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string>
<string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string> <string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string>
@ -17,13 +17,17 @@
<string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string> <string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string> <string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string> <string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string>
<string name="screen_room_change_role_confirm_change_owners_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol."</string>
<string name="screen_room_change_role_confirm_change_owners_title">"Trosglwyddo perchnogaeth?"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string> <string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string> <string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string> <string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string> <string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string>
<string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string> <string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string> <string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string>
<string name="screen_room_change_role_moderators_owner_section_footer">"Mae gan berchnogion freintiau gweinyddwr yn awtomatig."</string>
<string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string> <string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string>
<string name="screen_room_change_role_owners_title">"Dewiswch Berchnogion"</string>
<string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string> <string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string>
<string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string> <string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string>
<string name="screen_room_change_role_section_users">"Aelodau"</string> <string name="screen_room_change_role_section_users">"Aelodau"</string>
@ -48,15 +52,18 @@
<string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string> <string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string>
<string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string> <string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string>
<string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string> <string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string>
<string name="screen_room_member_list_role_owner">"Perchennog"</string>
<string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string> <string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string>
<string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string> <string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string>
<string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string> <string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string>
<string name="screen_room_roles_and_permissions_admins_and_owners">"Gweinyddwyr a pherchnogion"</string>
<string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string> <string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string>
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string> <string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string>
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string> <string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string>
<string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string> <string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string>
<string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string> <string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string>
<string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string> <string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string>
<string name="screen_room_roles_and_permissions_owners">"Perchnogion"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string> <string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string>
<string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string> <string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string> <string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string>

View file

@ -16,12 +16,12 @@
<string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string> <string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string>
<string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string> <string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string> <string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Lisää ylläpitäjä?"</string> <string name="screen_room_change_role_confirm_add_admin_title">"Lisätäänkö ylläpitäjä?"</string>
<string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string> <string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string>
<string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string> <string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string> <string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string> <string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Alenna itsesi?"</string> <string name="screen_room_change_role_confirm_demote_self_title">"Haluatko alentaa itsesi?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string> <string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string> <string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string> <string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string>
@ -32,7 +32,7 @@
<string name="screen_room_change_role_section_moderators">"Valvojat"</string> <string name="screen_room_change_role_section_moderators">"Valvojat"</string>
<string name="screen_room_change_role_section_users">"Jäsenet"</string> <string name="screen_room_change_role_section_users">"Jäsenet"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string> <string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string>
<string name="screen_room_change_role_unsaved_changes_title">"Tallenna muutokset?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Tallennetaanko muutokset?"</string>
<string name="screen_room_member_list_banned_empty">"Tässä huoneessa ei ole porttikieltoja"</string> <string name="screen_room_member_list_banned_empty">"Tässä huoneessa ei ole porttikieltoja"</string>
<plurals name="screen_room_member_list_header_title"> <plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d henkilö"</item> <item quantity="one">"%1$d henkilö"</item>

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_deactivate_account_list_item_3">"Delete your account information from our server."</string>
</resources>

View file

@ -15,9 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
interface FtueService { interface FtueService {
/** The current state of the FTUE. */ /** The current state of the FTUE. */
val state: StateFlow<FtueState> val state: StateFlow<FtueState>
/** Reset the FTUE state. */
suspend fun reset()
} }
/** The state of the FTUE. */ /** The state of the FTUE. */

View file

@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -30,18 +29,16 @@ import io.element.android.features.ftue.impl.notifications.NotificationsOptInNod
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -49,9 +46,8 @@ import kotlinx.parcelize.Parcelize
class FtueFlowNode( class FtueFlowNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val ftueState: DefaultFtueService, private val defaultFtueService: DefaultFtueService,
private val analyticsEntryPoint: AnalyticsEntryPoint, private val analyticsEntryPoint: AnalyticsEntryPoint,
private val analyticsService: AnalyticsService,
private val lockScreenEntryPoint: LockScreenEntryPoint, private val lockScreenEntryPoint: LockScreenEntryPoint,
) : BaseFlowNode<FtueFlowNode.NavTarget>( ) : BaseFlowNode<FtueFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -80,19 +76,11 @@ class FtueFlowNode(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
defaultFtueService.ftueStepStateFlow
lifecycle.subscribe(onCreate = { .filterIsInstance(InternalFtueState.Incomplete::class)
moveToNextStepIfNeeded() .onEach {
}) showStep(it.nextStep)
}
analyticsService.didAskUserConsentFlow
.distinctUntilChanged()
.onEach { moveToNextStepIfNeeded() }
.launchIn(lifecycleScope)
ftueState.isVerificationStatusKnown
.filter { it }
.onEach { moveToNextStepIfNeeded() }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
} }
@ -104,7 +92,7 @@ class FtueFlowNode(
is NavTarget.SessionVerification -> { is NavTarget.SessionVerification -> {
val callback = object : FtueSessionVerificationFlowNode.Callback { val callback = object : FtueSessionVerificationFlowNode.Callback {
override fun onDone() { override fun onDone() {
moveToNextStepIfNeeded() defaultFtueService.onUserCompletedSessionVerification()
} }
} }
createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback)) createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback))
@ -112,7 +100,7 @@ class FtueFlowNode(
NavTarget.NotificationsOptIn -> { NavTarget.NotificationsOptIn -> {
val callback = object : NotificationsOptInNode.Callback { val callback = object : NotificationsOptInNode.Callback {
override fun onNotificationsOptInFinished() { override fun onNotificationsOptInFinished() {
moveToNextStepIfNeeded() defaultFtueService.updateFtueStep()
} }
} }
createNode<NotificationsOptInNode>(buildContext, listOf(callback)) createNode<NotificationsOptInNode>(buildContext, listOf(callback))
@ -123,7 +111,7 @@ class FtueFlowNode(
NavTarget.LockScreenSetup -> { NavTarget.LockScreenSetup -> {
val callback = object : LockScreenEntryPoint.Callback { val callback = object : LockScreenEntryPoint.Callback {
override fun onSetupDone() { override fun onSetupDone() {
moveToNextStepIfNeeded() defaultFtueService.updateFtueStep()
} }
} }
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup) lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
@ -133,8 +121,8 @@ class FtueFlowNode(
} }
} }
private fun moveToNextStepIfNeeded() = lifecycleScope.launch { private fun showStep(ftueStep: FtueStep) {
when (ftueState.getNextStep()) { when (ftueStep) {
FtueStep.WaitingForInitialState -> { FtueStep.WaitingForInitialState -> {
backstack.newRoot(NavTarget.Placeholder) backstack.newRoot(NavTarget.Placeholder)
} }
@ -150,7 +138,6 @@ class FtueFlowNode(
FtueStep.LockscreenSetup -> { FtueStep.LockscreenSetup -> {
backstack.newRoot(NavTarget.LockScreenSetup) backstack.newRoot(NavTarget.LockScreenSetup)
} }
null -> Unit
} }
} }

View file

@ -9,13 +9,13 @@ package io.element.android.features.ftue.impl.state
import android.Manifest import android.Manifest
import android.os.Build import android.os.Build
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -26,61 +26,70 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@Inject @Inject
class DefaultFtueService( class DefaultFtueService(
private val sdkVersionProvider: BuildVersionSdkIntProvider, private val sdkVersionProvider: BuildVersionSdkIntProvider,
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val permissionStateProvider: PermissionStateProvider, private val permissionStateProvider: PermissionStateProvider,
private val lockScreenService: LockScreenService, private val lockScreenService: LockScreenService,
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val sessionPreferencesStore: SessionPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore,
) : FtueService { ) : FtueService {
override val state = MutableStateFlow<FtueState>(FtueState.Unknown) private val userNeedsToConfirmSessionVerificationSuccess = MutableStateFlow(false)
/** val ftueStepStateFlow = MutableStateFlow<InternalFtueState>(InternalFtueState.Unknown)
* This flow emits true when the FTUE flow is ready to be displayed.
* In this case, the FTUE flow is ready when the session verification status is known.
*/
val isVerificationStatusKnown = sessionVerificationService.sessionVerifiedStatus
.map { it != SessionVerifiedStatus.Unknown }
.distinctUntilChanged()
override suspend fun reset() { override val state = ftueStepStateFlow
analyticsService.reset() .mapState {
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { when (it) {
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS) is InternalFtueState.Unknown -> FtueState.Unknown
is InternalFtueState.Incomplete -> FtueState.Incomplete
is InternalFtueState.Complete -> FtueState.Complete
}
}
init {
combine(
sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus ->
if (sessionVerifiedStatus == SessionVerifiedStatus.NotVerified) {
// Ensure we wait for the user to confirm the session verified screen before going further
userNeedsToConfirmSessionVerificationSuccess.value = true
}
},
userNeedsToConfirmSessionVerificationSuccess,
analyticsService.didAskUserConsentFlow.distinctUntilChanged(),
) {
updateFtueStep()
}
.launchIn(sessionCoroutineScope)
}
fun updateFtueStep() = sessionCoroutineScope.launch {
val step = getNextStep(null)
ftueStepStateFlow.value = when (step) {
null -> InternalFtueState.Complete
else -> InternalFtueState.Incomplete(step)
} }
} }
init { private suspend fun getNextStep(completedStep: FtueStep? = null): FtueStep? =
sessionVerificationService.sessionVerifiedStatus when (completedStep) {
.onEach { updateState() }
.launchIn(sessionCoroutineScope)
analyticsService.didAskUserConsentFlow
.distinctUntilChanged()
.onEach { updateState() }
.launchIn(sessionCoroutineScope)
}
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
when (currentStep) {
null -> if (!isSessionVerificationStateReady()) { null -> if (!isSessionVerificationStateReady()) {
FtueStep.WaitingForInitialState FtueStep.WaitingForInitialState
} else { } else {
getNextStep(FtueStep.WaitingForInitialState) getNextStep(FtueStep.WaitingForInitialState)
} }
FtueStep.WaitingForInitialState -> if (isSessionNotVerified()) { FtueStep.WaitingForInitialState -> if (isSessionNotVerified() || userNeedsToConfirmSessionVerificationSuccess.value) {
FtueStep.SessionVerification FtueStep.SessionVerification
} else { } else {
getNextStep(FtueStep.SessionVerification) getNextStep(FtueStep.SessionVerification)
@ -108,9 +117,6 @@ class DefaultFtueService(
} }
private suspend fun isSessionNotVerified(): Boolean { private suspend fun isSessionNotVerified(): Boolean {
// Wait until the session verification status is known
isVerificationStatusKnown.filter { it }.first()
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification() return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification()
} }
@ -137,14 +143,8 @@ class DefaultFtueService(
return lockScreenService.isSetupRequired().first() return lockScreenService.isSetupRequired().first()
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun onUserCompletedSessionVerification() {
internal suspend fun updateState() { userNeedsToConfirmSessionVerificationSuccess.value = false
val nextStep = getNextStep()
state.value = when {
// Final state, there aren't any more next steps
nextStep == null -> FtueState.Complete
else -> FtueState.Incomplete
}
} }
} }

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.ftue.impl.state
sealed interface InternalFtueState {
data object Unknown : InternalFtueState
data class Incomplete(
val nextStep: FtueStep,
) : InternalFtueState
data object Complete : InternalFtueState
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new backup password"</string>
<string name="screen_identity_confirmation_subtitle">"Confirm this device to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm it\'s you"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Use backup password"</string>
<string name="screen_identity_confirmed_title">"Device confirmed"</string>
<string name="screen_session_verification_enter_recovery_key">"Enter backup password"</string>
</resources>

View file

@ -14,7 +14,6 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.node.TestParentNode
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -36,8 +35,7 @@ class DefaultFtueEntryPointTest {
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
analyticsEntryPoint = { _, _ -> lambdaError() }, analyticsEntryPoint = { _, _ -> lambdaError() },
ftueState = createDefaultFtueService(), defaultFtueService = createDefaultFtueService(),
analyticsService = FakeAnalyticsService(),
lockScreenEntryPoint = object : LockScreenEntryPoint { lockScreenEntryPoint = object : LockScreenEntryPoint {
override fun nodeBuilder( override fun nodeBuilder(
parentNode: com.bumble.appyx.core.node.Node, parentNode: com.bumble.appyx.core.node.Node,

View file

@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.DefaultFtueService
import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.state.InternalFtueState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -26,8 +27,6 @@ import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -69,9 +68,11 @@ class DefaultFtueServiceTest {
analyticsService.setDidAskUserConsent() analyticsService.setDidAskUserConsent()
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
service.updateState() service.updateFtueStep()
service.state.test {
assertThat(service.state.value).isEqualTo(FtueState.Complete) assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
}
} }
@Test @Test
@ -90,9 +91,11 @@ class DefaultFtueServiceTest {
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
service.updateState() service.updateFtueStep()
service.state.test {
assertThat(service.state.value).isEqualTo(FtueState.Complete) assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
}
} }
@Test @Test
@ -109,35 +112,30 @@ class DefaultFtueServiceTest {
permissionStateProvider = permissionStateProvider, permissionStateProvider = permissionStateProvider,
lockScreenService = lockScreenService, lockScreenService = lockScreenService,
) )
val steps = mutableListOf<FtueStep?>()
// Session verification service.ftueStepStateFlow.test {
steps.add(service.getNextStep(steps.lastOrNull())) assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified) // Session verification
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.SessionVerification))
// Notifications opt in sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
steps.add(service.getNextStep(steps.lastOrNull())) // User completes verification
permissionStateProvider.setPermissionGranted() service.onUserCompletedSessionVerification()
// Notifications opt in
// Entering PIN code assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.NotificationsOptIn))
steps.add(service.getNextStep(steps.lastOrNull())) permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) // Simulate event from NotificationsOptInNode.Callback.onNotificationsOptInFinished
service.updateFtueStep()
// Analytics opt in // Entering PIN code
steps.add(service.getNextStep(steps.lastOrNull())) assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.LockscreenSetup))
analyticsService.setDidAskUserConsent() lockScreenService.setIsPinSetup(true)
// Simulate event from LockScreenEntryPoint.Callback.onSetupDone()
// Final step (null) service.updateFtueStep()
steps.add(service.getNextStep(steps.lastOrNull())) // Analytics opt in
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
assertThat(steps).containsExactly( analyticsService.setDidAskUserConsent()
FtueStep.SessionVerification, // Final step
FtueStep.NotificationsOptIn, assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
FtueStep.LockscreenSetup, }
FtueStep.AnalyticsOptIn,
// Final state
null,
)
} }
@Test @Test
@ -158,10 +156,13 @@ class DefaultFtueServiceTest {
permissionStateProvider.setPermissionGranted() permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) service.ftueStepStateFlow.test {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
analyticsService.setDidAskUserConsent() // Analytics opt in
assertThat(service.getNextStep(null)).isNull() assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
analyticsService.setDidAskUserConsent()
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
}
} }
@Test @Test
@ -180,51 +181,13 @@ class DefaultFtueServiceTest {
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
lockScreenService.setIsPinSetup(true) lockScreenService.setIsPinSetup(true)
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) service.ftueStepStateFlow.test {
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
analyticsService.setDidAskUserConsent() // Analytics opt in
assertThat(service.getNextStep(null)).isNull() assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
} analyticsService.setDidAskUserConsent()
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
@Test }
fun `reset do the expected actions S`() = runTest {
val resetAnalyticsLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetAnalyticsLambda
)
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
val permissionStateProvider = FakePermissionStateProvider(
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.S,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
)
service.reset()
resetAnalyticsLambda.assertions().isCalledOnce()
resetPermissionLambda.assertions().isNeverCalled()
}
@Test
fun `reset do the expected actions TIRAMISU`() = runTest {
val resetLambda = lambdaRecorder<Unit> { }
val analyticsService = FakeAnalyticsService(
resetLambda = resetLambda
)
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
val permissionStateProvider = FakePermissionStateProvider(
resetPermissionLambda = resetPermissionLambda
)
val service = createDefaultFtueService(
sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
permissionStateProvider = permissionStateProvider,
analyticsService = analyticsService,
)
service.reset()
resetLambda.assertions().isCalledOnce()
resetPermissionLambda.assertions().isCalledOnce()
.with(value("android.permission.POST_NOTIFICATIONS"))
} }
} }

View file

@ -9,18 +9,11 @@ package io.element.android.features.ftue.test
import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.api.state.FtueState
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeFtueService( class FakeFtueService : FtueService {
private val resetLambda: () -> Unit = { lambdaError() },
) : FtueService {
override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown) override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown)
override suspend fun reset() {
resetLambda()
}
suspend fun emitState(newState: FtueState) { suspend fun emitState(newState: FtueState) {
state.emit(newState) state.emit(newState)
} }

View file

@ -13,6 +13,7 @@
<string name="full_screen_intent_banner_message">"Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi."</string> <string name="full_screen_intent_banner_message">"Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi."</string>
<string name="full_screen_intent_banner_title">"Gwella profiad eich galwadau"</string> <string name="full_screen_intent_banner_title">"Gwella profiad eich galwadau"</string>
<string name="screen_home_tab_chats">"Sgyrsiau"</string> <string name="screen_home_tab_chats">"Sgyrsiau"</string>
<string name="screen_home_tab_spaces">"Gofodau"</string>
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string> <string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string> <string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string> <string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
@ -32,6 +33,7 @@ Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</strin
<string name="screen_roomlist_filter_invites">"Gwahoddiadau"</string> <string name="screen_roomlist_filter_invites">"Gwahoddiadau"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Does gennych chi ddim gwahoddiadau yn aros."</string> <string name="screen_roomlist_filter_invites_empty_state_title">"Does gennych chi ddim gwahoddiadau yn aros."</string>
<string name="screen_roomlist_filter_low_priority">"Blaenoriaeth Isel"</string> <string name="screen_roomlist_filter_low_priority">"Blaenoriaeth Isel"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Does gennych chi ddim sgyrsiau blaenoriaeth isel eto"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</string> <string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn"</string> <string name="screen_roomlist_filter_mixed_empty_state_title">"Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn"</string>
<string name="screen_roomlist_filter_people">"Pobl"</string> <string name="screen_roomlist_filter_people">"Pobl"</string>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_set_up_recovery_content">"Restore your account security and message history with a backup password if you have lost all your existing devices."</string>
<string name="banner_set_up_recovery_submit">"Set up backup"</string>
<string name="banner_set_up_recovery_title">"Set up backup to protect your account"</string>
<string name="confirm_recovery_key_banner_message">"Confirm your backup password to maintain access to your message backup and message history."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another connected device to access your encrypted messages."</string>
</resources>

View file

@ -33,6 +33,7 @@ Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."<
<string name="screen_roomlist_filter_invites">"Kutsut"</string> <string name="screen_roomlist_filter_invites">"Kutsut"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"Sinulla ei ole yhtään odottavaa kutsua."</string> <string name="screen_roomlist_filter_invites_empty_state_title">"Sinulla ei ole yhtään odottavaa kutsua."</string>
<string name="screen_roomlist_filter_low_priority">"Matala prioriteetti"</string> <string name="screen_roomlist_filter_low_priority">"Matala prioriteetti"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sinulla ei ole vielä yhtään matalan prioriteetin keskustelua"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi."</string> <string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi."</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Sinulla ei ole sopivia keskusteluja tähän valintaan"</string> <string name="screen_roomlist_filter_mixed_empty_state_title">"Sinulla ei ole sopivia keskusteluja tähän valintaan"</string>
<string name="screen_roomlist_filter_people">"Ihmiset"</string> <string name="screen_roomlist_filter_people">"Ihmiset"</string>

View file

@ -13,6 +13,7 @@
<string name="full_screen_intent_banner_message">"For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst."</string> <string name="full_screen_intent_banner_message">"For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst."</string>
<string name="full_screen_intent_banner_title">"Forbedre samtaleopplevelsen din"</string> <string name="full_screen_intent_banner_title">"Forbedre samtaleopplevelsen din"</string>
<string name="screen_home_tab_chats">"Chatter"</string> <string name="screen_home_tab_chats">"Chatter"</string>
<string name="screen_home_tab_spaces">"Områder"</string>
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string> <string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string> <string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string> <string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>

View file

@ -33,6 +33,7 @@
<string name="screen_roomlist_filter_invites">"邀請"</string> <string name="screen_roomlist_filter_invites">"邀請"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"您沒有任何擱置中的邀請。"</string> <string name="screen_roomlist_filter_invites_empty_state_title">"您沒有任何擱置中的邀請。"</string>
<string name="screen_roomlist_filter_low_priority">"低優先度"</string> <string name="screen_roomlist_filter_low_priority">"低優先度"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"您尚無任何低優先程度聊天"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"您可以取消選取篩選條件以檢視其他聊天"</string> <string name="screen_roomlist_filter_mixed_empty_state_subtitle">"您可以取消選取篩選條件以檢視其他聊天"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"您並無此選擇的聊天"</string> <string name="screen_roomlist_filter_mixed_empty_state_title">"您並無此選擇的聊天"</string>
<string name="screen_roomlist_filter_people">"夥伴"</string> <string name="screen_roomlist_filter_people">"夥伴"</string>

View file

@ -18,6 +18,7 @@
<string name="screen_join_room_join_action">"Ymuno â\'r ystafell"</string> <string name="screen_join_room_join_action">"Ymuno â\'r ystafell"</string>
<string name="screen_join_room_join_restricted_message">"Efallai y bydd angen i chi gael eich gwahodd neu fod yn aelod o ofod er mwyn ymuno."</string> <string name="screen_join_room_join_restricted_message">"Efallai y bydd angen i chi gael eich gwahodd neu fod yn aelod o ofod er mwyn ymuno."</string>
<string name="screen_join_room_knock_action">"Anfon cais i ymuno"</string> <string name="screen_join_room_knock_action">"Anfon cais i ymuno"</string>
<string name="screen_join_room_knock_message_characters_count">"Nodau a ganiateir %1$d o %2$d"</string>
<string name="screen_join_room_knock_message_description">"Neges (dewisol)"</string> <string name="screen_join_room_knock_message_description">"Neges (dewisol)"</string>
<string name="screen_join_room_knock_sent_description">"Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn."</string> <string name="screen_join_room_knock_sent_description">"Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn."</string>
<string name="screen_join_room_knock_sent_title">"Anfonwyd y cais i ymuno"</string> <string name="screen_join_room_knock_sent_title">"Anfonwyd y cais i ymuno"</string>

View file

@ -3,5 +3,8 @@
<string name="leave_conversation_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael y sgwrs hon? Dyw\'r sgwrs hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string> <string name="leave_conversation_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael y sgwrs hon? Dyw\'r sgwrs hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string>
<string name="leave_room_alert_empty_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Chi yw\'r unig berson yma. Os byddwch yn gadael, fydd neb yn gallu ymuno yn y dyfodol, gan gynnwys chi."</string> <string name="leave_room_alert_empty_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Chi yw\'r unig berson yma. Os byddwch yn gadael, fydd neb yn gallu ymuno yn y dyfodol, gan gynnwys chi."</string>
<string name="leave_room_alert_private_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Dyw\'r ystafell hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string> <string name="leave_room_alert_private_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Dyw\'r ystafell hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string>
<string name="leave_room_alert_select_new_owner_action">"Dewiswch Berchnogion"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"Chi yw unig berchennog yr ystafell hon. Mae angen i chi drosglwyddo perchnogaeth i rywun arall cyn i chi adael yr room."</string>
<string name="leave_room_alert_select_new_owner_title">"Trosglwyddo perchnogaeth"</string>
<string name="leave_room_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell?"</string> <string name="leave_room_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell?"</string>
</resources> </resources>

View file

@ -9,7 +9,7 @@
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string> <string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
<string name="screen_app_lock_settings_remove_pin">"Poista PIN-koodi"</string> <string name="screen_app_lock_settings_remove_pin">"Poista PIN-koodi"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Haluatko varmasti poistaa PIN-koodin?"</string> <string name="screen_app_lock_settings_remove_pin_alert_message">"Haluatko varmasti poistaa PIN-koodin?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Poista PIN-koodi?"</string> <string name="screen_app_lock_settings_remove_pin_alert_title">"Poistetaanko PIN-koodi?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Salli %1$s"</string> <string name="screen_app_lock_setup_biometric_unlock_allow_title">"Salli %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Käytän mieluummin PIN-koodia"</string> <string name="screen_app_lock_setup_biometric_unlock_skip">"Käytän mieluummin PIN-koodia"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästä aikaa ja ota käyttöön %1$s"</string> <string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästä aikaa ja ota käyttöön %1$s"</string>

View file

@ -13,6 +13,9 @@
<string name="screen_change_account_provider_other">"Arall"</string> <string name="screen_change_account_provider_other">"Arall"</string>
<string name="screen_change_account_provider_subtitle">"Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith."</string> <string name="screen_change_account_provider_subtitle">"Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith."</string>
<string name="screen_change_account_provider_title">"Newid darparwr cyfrif"</string> <string name="screen_change_account_provider_title">"Newid darparwr cyfrif"</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">"Mae angen yr ap Element Pro ar %1$s. Llwythwch ef o\'r siop."</string>
<string name="screen_change_server_error_element_pro_required_title">"Mae angen Element Pro"</string>
<string name="screen_change_server_error_invalid_homeserver">"Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help."</string> <string name="screen_change_server_error_invalid_homeserver">"Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help."</string>
<string name="screen_change_server_error_invalid_well_known">"Dyw cydweddu llithrig ddim ar gael oherwydd problem yn y ffeil .well-known: <string name="screen_change_server_error_invalid_well_known">"Dyw cydweddu llithrig ddim ar gael oherwydd problem yn y ffeil .well-known:
%1$s"</string> %1$s"</string>

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_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing connected devices are still safe and you don\'t need to worry about them."</string>
</resources>

View file

@ -13,6 +13,7 @@
<string name="screen_change_account_provider_other">"Annet"</string> <string name="screen_change_account_provider_other">"Annet"</string>
<string name="screen_change_account_provider_subtitle">"Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto."</string> <string name="screen_change_account_provider_subtitle">"Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto."</string>
<string name="screen_change_account_provider_title">"Bytt kontotilbyder"</string> <string name="screen_change_account_provider_title">"Bytt kontotilbyder"</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-appen er nødvendig på %1$s. Last den ned fra butikken."</string> <string name="screen_change_server_error_element_pro_required_message">"Element Pro-appen er nødvendig på %1$s. Last den ned fra butikken."</string>
<string name="screen_change_server_error_element_pro_required_title">"Element Pro kreves"</string> <string name="screen_change_server_error_element_pro_required_title">"Element Pro kreves"</string>
<string name="screen_change_server_error_invalid_homeserver">"Vi kunne ikke nå denne hjemmeserveren. Kontroller at du har skrevet inn hjemmeserverens URL riktig. Hvis URL-en er riktig, kontakt administratoren for hjemmeserveren din for å få mer hjelp."</string> <string name="screen_change_server_error_invalid_homeserver">"Vi kunne ikke nå denne hjemmeserveren. Kontroller at du har skrevet inn hjemmeserverens URL riktig. Hvis URL-en er riktig, kontakt administratoren for hjemmeserveren din for å få mer hjelp."</string>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signout_key_backup_offline_subtitle">"Your messages were still being backed up when you went offline. Reconnect so that your messages can be backed up before signing out."</string>
<string name="screen_signout_key_backup_offline_title">"Your messages are still being backed up"</string>
<string name="screen_signout_key_backup_ongoing_title">"Your messages are still being backed up"</string>
<string name="screen_signout_recovery_disabled_title">"Backup not set up"</string>
<string name="screen_signout_save_recovery_key_title">"Have you saved your backup password?"</string>
</resources>

View file

@ -87,6 +87,7 @@ import io.element.android.libraries.designsystem.components.ExpandableBottomShee
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState
import io.element.android.libraries.designsystem.components.avatar.Avatar 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.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
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@ -95,6 +96,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.text.toAnnotatedString
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
@ -111,10 +113,14 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.link.Link import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -202,7 +208,13 @@ fun MessagesView(
Column { Column {
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
if (state.timelineState.timelineMode is Timeline.Mode.Thread) { if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
ThreadTopBar(onBackClick = onBackClick) ThreadTopBar(
roomName = state.roomName,
roomAvatarData = state.roomAvatar,
heroes = state.heroes,
isTombstoned = state.isTombstoned,
onBackClick = onBackClick,
)
} else { } else {
MessagesViewTopBar( MessagesViewTopBar(
roomName = state.roomName, roomName = state.roomName,
@ -573,14 +585,48 @@ private fun MessagesViewTopBar(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ThreadTopBar( private fun ThreadTopBar(
roomName: String?,
roomAvatarData: AvatarData,
heroes: ImmutableList<AvatarData>,
isTombstoned: Boolean,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
TopAppBar( TopAppBar(
modifier = modifier,
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackClick) BackButton(onClick = onBackClick)
}, },
title = { title = {
Text(stringResource(CommonStrings.common_thread)) Row(verticalAlignment = Alignment.CenterVertically) {
Avatar(
avatarData = roomAvatarData,
avatarType = AvatarType.Room(
heroes = heroes,
isTombstoned = isTombstoned,
),
)
Column(
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 8.dp)
.semantics {
heading()
},
) {
Text(
text = stringResource(CommonStrings.common_thread),
style = ElementTheme.typography.fontBodyLgMedium,
)
Text(
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
style = ElementTheme.typography.fontBodySmRegular,
fontStyle = FontStyle.Italic.takeIf { roomName == null },
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
} }
) )
} }
@ -673,3 +719,58 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
knockRequestsBannerView = {}, knockRequestsBannerView = {},
) )
} }
@PreviewsDayNight
@Composable
internal fun ThreadTopBarPreview() {
ElementPreview {
val name = "Room name"
val initialsAvatarData = AvatarData(
id = "id",
name = name,
url = null,
size = AvatarSize.TimelineRoom,
)
Column {
ThreadTopBar(
roomName = name,
roomAvatarData = initialsAvatarData,
heroes = persistentListOf(),
isTombstoned = false,
onBackClick = {},
)
HorizontalDivider()
ThreadTopBar(
roomName = name,
roomAvatarData = initialsAvatarData,
heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(),
isTombstoned = false,
onBackClick = {},
)
HorizontalDivider()
ThreadTopBar(
roomName = null,
roomAvatarData = initialsAvatarData,
heroes = persistentListOf(),
isTombstoned = false,
onBackClick = {},
)
HorizontalDivider()
ThreadTopBar(
roomName = name,
roomAvatarData = initialsAvatarData.copy(url = "https://some-avatar.jpg"),
heroes = persistentListOf(),
isTombstoned = false,
onBackClick = {},
)
HorizontalDivider()
ThreadTopBar(
roomName = name,
roomAvatarData = initialsAvatarData,
heroes = persistentListOf(),
isTombstoned = true,
onBackClick = {},
)
}
}
}

View file

@ -7,6 +7,7 @@
<string name="emoji_picker_category_objects">"Předměty"</string> <string name="emoji_picker_category_objects">"Předměty"</string>
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string> <string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
<string name="emoji_picker_category_places">"Cestování a místa"</string> <string name="emoji_picker_category_places">"Cestování a místa"</string>
<string name="emoji_picker_category_recent">"Nedávné emotikony"</string>
<string name="emoji_picker_category_symbols">"Symboly"</string> <string name="emoji_picker_category_symbols">"Symboly"</string>
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string> <string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string>
@ -15,6 +16,7 @@
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string> <string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Maximální povolená velikost souboru je %1$s."</string> <string name="screen_media_upload_preview_error_too_large_message">"Maximální povolená velikost souboru je %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Soubor je pro nahrání příliš velký."</string> <string name="screen_media_upload_preview_error_too_large_title">"Soubor je pro nahrání příliš velký."</string>
<string name="screen_media_upload_preview_item_count">"Položka %1$d z %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalizace kvality obrazu"</string> <string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalizace kvality obrazu"</string>
<string name="screen_media_upload_preview_processing">"Probíhá zpracování…"</string> <string name="screen_media_upload_preview_processing">"Probíhá zpracování…"</string>
<string name="screen_report_content_block_user">"Zablokovat uživatele"</string> <string name="screen_report_content_block_user">"Zablokovat uživatele"</string>

View file

@ -7,10 +7,18 @@
<string name="emoji_picker_category_objects">"Gwrthrychau"</string> <string name="emoji_picker_category_objects">"Gwrthrychau"</string>
<string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string> <string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string>
<string name="emoji_picker_category_places">"Teithio a Llefydd"</string> <string name="emoji_picker_category_places">"Teithio a Llefydd"</string>
<string name="emoji_picker_category_recent">"Emojis diweddar"</string>
<string name="emoji_picker_category_symbols">"Symbolau"</string> <string name="emoji_picker_category_symbols">"Symbolau"</string>
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string> <string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tapiwch i newid ansawdd llwytho\'r fideo"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nid oedd modd llwytho\'r ffeil."</string>
<string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string> <string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
<string name="screen_media_upload_preview_error_failed_sending">"Wedi methu llwytho cyfryngau, ceisiwch eto."</string> <string name="screen_media_upload_preview_error_failed_sending">"Wedi methu llwytho cyfryngau, ceisiwch eto."</string>
<string name="screen_media_upload_preview_error_too_large_message">"Y maint ffeil mwyaf a ganiateir yw %1$s ."</string>
<string name="screen_media_upload_preview_error_too_large_title">"Mae\'r ffeil yn rhy fawr i\'w llwytho"</string>
<string name="screen_media_upload_preview_item_count">"Eitem %1$d o %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimeiddio ansawdd delwedd"</string>
<string name="screen_media_upload_preview_processing">"Prosesu…"</string>
<string name="screen_report_content_block_user">"Rhwystro defnyddiwr"</string> <string name="screen_report_content_block_user">"Rhwystro defnyddiwr"</string>
<string name="screen_report_content_block_user_hint">"Gwiriwch a ydych am guddio\'r holl negeseuon presennol ac yn y dyfodol gan y defnyddiwr hwn"</string> <string name="screen_report_content_block_user_hint">"Gwiriwch a ydych am guddio\'r holl negeseuon presennol ac yn y dyfodol gan y defnyddiwr hwn"</string>
<string name="screen_report_content_explanation">"Bydd y neges hon yn cael ei hadrodd i weinyddwr eich gweinyddwr cartref. Fyddan nhw ddim yn gallu darllen unrhyw negeseuon wedi\'u hamgryptio."</string> <string name="screen_report_content_explanation">"Bydd y neges hon yn cael ei hadrodd i weinyddwr eich gweinyddwr cartref. Fyddan nhw ddim yn gallu darllen unrhyw negeseuon wedi\'u hamgryptio."</string>
@ -38,6 +46,14 @@
<string name="screen_room_timeline_less_reactions">"Dangos llai"</string> <string name="screen_room_timeline_less_reactions">"Dangos llai"</string>
<string name="screen_room_timeline_message_copied">"Neges wedi\'i chopïo"</string> <string name="screen_room_timeline_message_copied">"Neges wedi\'i chopïo"</string>
<string name="screen_room_timeline_no_permission_to_post">"Does gennych chi ddim caniatâd i bostio i\'r ystafell hon"</string> <string name="screen_room_timeline_no_permission_to_post">"Does gennych chi ddim caniatâd i bostio i\'r ystafell hon"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="zero">"Ymatebodd %1$d aelodau gyda %2$s"</item>
<item quantity="one">"Ymatebodd %1$d aelodau gyda %2$s"</item>
<item quantity="two">"Ymatebodd %1$d aelod gyda %2$s"</item>
<item quantity="few">"Ymatebodd %1$d aelod gyda %2$s"</item>
<item quantity="many">"Ymatebodd %1$d aelod gyda %2$s"</item>
<item quantity="other">"Ymatebodd %1$d aelod gyda %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"Rydych chi wedi ymateb gyda %1$s"</string> <string name="screen_room_timeline_reaction_you_a11y">"Rydych chi wedi ymateb gyda %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Dangos llai"</string> <string name="screen_room_timeline_reactions_show_less">"Dangos llai"</string>
<string name="screen_room_timeline_reactions_show_more">"Dangos rhagor"</string> <string name="screen_room_timeline_reactions_show_more">"Dangos rhagor"</string>

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_room_encrypted_history_banner_unverified">"Message history is unavailable in this room. Confirm this device to see your message history."</string>
</resources>

View file

@ -7,6 +7,7 @@
<string name="emoji_picker_category_objects">"Esineet"</string> <string name="emoji_picker_category_objects">"Esineet"</string>
<string name="emoji_picker_category_people">"Hymiöt ja ihmiset"</string> <string name="emoji_picker_category_people">"Hymiöt ja ihmiset"</string>
<string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string> <string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string>
<string name="emoji_picker_category_recent">"Viimeaikaiset emojit"</string>
<string name="emoji_picker_category_symbols">"Symbolit"</string> <string name="emoji_picker_category_symbols">"Symbolit"</string>
<string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string> <string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Napauta muuttaaksesi videon lähetyslaatua"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Napauta muuttaaksesi videon lähetyslaatua"</string>

View file

@ -9,11 +9,14 @@
<string name="emoji_picker_category_places">"旅行與景點"</string> <string name="emoji_picker_category_places">"旅行與景點"</string>
<string name="emoji_picker_category_symbols">"標誌"</string> <string name="emoji_picker_category_symbols">"標誌"</string>
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string> <string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string> <string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
<string name="screen_media_upload_preview_error_failed_processing">"無法處理要上傳的媒體,請再試一次。"</string> <string name="screen_media_upload_preview_error_failed_processing">"無法處理要上傳的媒體,請再試一次。"</string>
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string> <string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
<string name="screen_media_upload_preview_error_too_large_message">"允許的最大檔案大小為 %1$s。"</string> <string name="screen_media_upload_preview_error_too_large_message">"允許的最大檔案大小為 %1$s。"</string>
<string name="screen_media_upload_preview_error_too_large_title">"檔案太大,無法上傳"</string> <string name="screen_media_upload_preview_error_too_large_title">"檔案太大,無法上傳"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"最佳化影像品質"</string>
<string name="screen_media_upload_preview_processing">"正在處理……"</string>
<string name="screen_report_content_block_user">"封鎖使用者"</string> <string name="screen_report_content_block_user">"封鎖使用者"</string>
<string name="screen_report_content_block_user_hint">"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"</string> <string name="screen_report_content_block_user_hint">"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"</string>
<string name="screen_report_content_explanation">"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"</string> <string name="screen_report_content_explanation">"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"</string>

View file

@ -7,6 +7,7 @@
<string name="emoji_picker_category_objects">"Objects"</string> <string name="emoji_picker_category_objects">"Objects"</string>
<string name="emoji_picker_category_people">"Smileys &amp; People"</string> <string name="emoji_picker_category_people">"Smileys &amp; People"</string>
<string name="emoji_picker_category_places">"Travel &amp; Places"</string> <string name="emoji_picker_category_places">"Travel &amp; Places"</string>
<string name="emoji_picker_category_recent">"Recent emojis"</string>
<string name="emoji_picker_category_symbols">"Symbols"</string> <string name="emoji_picker_category_symbols">"Symbols"</string>
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string> <string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tap to change the video upload quality"</string> <string name="screen_media_upload_preview_change_video_quality_prompt">"Tap to change the video upload quality"</string>
@ -15,6 +16,7 @@
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string> <string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
<string name="screen_media_upload_preview_error_too_large_message">"The maximum file size allowed is %1$s."</string> <string name="screen_media_upload_preview_error_too_large_message">"The maximum file size allowed is %1$s."</string>
<string name="screen_media_upload_preview_error_too_large_title">"The file is too large to upload"</string> <string name="screen_media_upload_preview_error_too_large_title">"The file is too large to upload"</string>
<string name="screen_media_upload_preview_item_count">"Item %1$d of %2$d"</string>
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimise image quality"</string> <string name="screen_media_upload_preview_optimize_image_quality_title">"Optimise image quality"</string>
<string name="screen_media_upload_preview_processing">"Processing…"</string> <string name="screen_media_upload_preview_processing">"Processing…"</string>
<string name="screen_report_content_block_user">"Block user"</string> <string name="screen_report_content_block_user">"Block user"</string>

View file

@ -1,5 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="zero">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="one">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="two">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="few">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="many">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="other">"%1$d y cant o\'r holl bleidleisiau"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Bydd yn dileu\'r dewis blaenorol"</string> <string name="a11y_polls_will_remove_selection">"Bydd yn dileu\'r dewis blaenorol"</string>
<string name="a11y_polls_winning_answer">"Dyma\'r ateb buddugol"</string> <string name="a11y_polls_winning_answer">"Dyma\'r ateb buddugol"</string>
</resources> </resources>

View file

@ -72,7 +72,6 @@ dependencies {
implementation(projects.features.rageshake.api) implementation(projects.features.rageshake.api)
implementation(projects.features.lockscreen.api) implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api) implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api)
implementation(projects.features.licenses.api) implementation(projects.features.licenses.api)
implementation(projects.features.logout.api) implementation(projects.features.logout.api)
implementation(projects.features.deactivation.api) implementation(projects.features.deactivation.api)
@ -101,7 +100,6 @@ dependencies {
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.features.ftue.test)
testImplementation(projects.features.invite.test) testImplementation(projects.features.invite.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.logout.test) testImplementation(projects.features.logout.test)

View file

@ -208,6 +208,10 @@ class PreferencesFlowNode(
navigateUp() navigateUp()
} }
} }
override fun openIgnoredUsers() {
backstack.push(NavTarget.BlockedUsers)
}
}) })
.build() .build()
} }

View file

@ -12,7 +12,6 @@ import coil3.SingletonImageLoader
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.Provider import dev.zacsweers.metro.Provider
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -36,7 +35,6 @@ class DefaultClearCacheUseCase(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
private val defaultCacheService: DefaultCacheService, private val defaultCacheService: DefaultCacheService,
private val okHttpClient: Provider<OkHttpClient>, private val okHttpClient: Provider<OkHttpClient>,
private val ftueService: FtueService,
private val pushService: PushService, private val pushService: PushService,
private val seenInvitesStore: SeenInvitesStore, private val seenInvitesStore: SeenInvitesStore,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
@ -56,7 +54,6 @@ class DefaultClearCacheUseCase(
// Clear app cache // Clear app cache
context.cacheDir.deleteRecursively() context.cacheDir.deleteRecursively()
// Clear some settings // Clear some settings
ftueService.reset()
seenInvitesStore.clear() seenInvitesStore.clear()
// Ensure any error will be displayed again // Ensure any error will be displayed again
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false) pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)

View file

@ -13,6 +13,13 @@
<string name="screen_advanced_settings_media_compression_description">"Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata"</string> <string name="screen_advanced_settings_media_compression_description">"Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata"</string>
<string name="screen_advanced_settings_media_compression_title">"Optimeiddio ansawdd y cyfryngau"</string> <string name="screen_advanced_settings_media_compression_title">"Optimeiddio ansawdd y cyfryngau"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Cymedroli a Diogelwch"</string> <string name="screen_advanced_settings_moderation_and_safety_section_title">"Cymedroli a Diogelwch"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimeiddio delweddau\'n awtomatig ar gyfer llwytho cyflymach a meintiau ffeiliau llai."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimeiddio ansawdd llwytho delweddau"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s Tapiwch yma i newid."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Uchel (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Isel (480c)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Safonol (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Ansawdd lwytho fideo"</string>
<string name="screen_advanced_settings_push_provider_android">"Darparwr hysbysiad gwthio"</string> <string name="screen_advanced_settings_push_provider_android">"Darparwr hysbysiad gwthio"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw."</string> <string name="screen_advanced_settings_rich_text_editor_description">"Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw."</string>
<string name="screen_advanced_settings_send_read_receipts">"Derbynebau darllen"</string> <string name="screen_advanced_settings_send_read_receipts">"Derbynebau darllen"</string>

View file

@ -16,6 +16,9 @@
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimoi kuvat automaattisesti nopeampia lähetysnopeuksia ja pienempiä tiedostokokoja varten."</string> <string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimoi kuvat automaattisesti nopeampia lähetysnopeuksia ja pienempiä tiedostokokoja varten."</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimoi kuvien lähetyslaatu"</string> <string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimoi kuvien lähetyslaatu"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Napauta tästä vaihtaaksesi."</string> <string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Napauta tästä vaihtaaksesi."</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Korkea (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Matala (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Normaali (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Videon lähetyslaatu"</string> <string name="screen_advanced_settings_optimise_video_upload_quality_title">"Videon lähetyslaatu"</string>
<string name="screen_advanced_settings_push_provider_android">"Push-ilmoitusten tarjoaja"</string> <string name="screen_advanced_settings_push_provider_android">"Push-ilmoitusten tarjoaja"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti."</string> <string name="screen_advanced_settings_rich_text_editor_description">"Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti."</string>

View file

@ -13,6 +13,13 @@
<string name="screen_advanced_settings_media_compression_description">"上傳照片與影片更快且減少資料使用量"</string> <string name="screen_advanced_settings_media_compression_description">"上傳照片與影片更快且減少資料使用量"</string>
<string name="screen_advanced_settings_media_compression_title">"最佳化媒體品質"</string> <string name="screen_advanced_settings_media_compression_title">"最佳化媒體品質"</string>
<string name="screen_advanced_settings_moderation_and_safety_section_title">"管理與安全"</string> <string name="screen_advanced_settings_moderation_and_safety_section_title">"管理與安全"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"自動最佳化影像以提供更快的上傳速度與較小的檔案大小。"</string>
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"最佳化影像上傳品質"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s。輕點此處以變更。"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"高 (1080p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"低 (480p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"標準 (720p)"</string>
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"視訊上傳品質"</string>
<string name="screen_advanced_settings_push_provider_android">"推播通知提供者"</string> <string name="screen_advanced_settings_push_provider_android">"推播通知提供者"</string>
<string name="screen_advanced_settings_rich_text_editor_description">"手動輸入 Markdown停用格式化文字編輯器。"</string> <string name="screen_advanced_settings_rich_text_editor_description">"手動輸入 Markdown停用格式化文字編輯器。"</string>
<string name="screen_advanced_settings_send_read_receipts">"已讀回條"</string> <string name="screen_advanced_settings_send_read_receipts">"已讀回條"</string>

View file

@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.tasks
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.ftue.test.FakeFtueService
import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -41,10 +40,6 @@ class DefaultClearCacheUseCaseTest {
clearCacheLambda = clearCacheLambda, clearCacheLambda = clearCacheLambda,
) )
val defaultCacheService = DefaultCacheService() val defaultCacheService = DefaultCacheService()
val resetFtueLambda = lambdaRecorder<Unit> { }
val ftueService = FakeFtueService(
resetLambda = resetFtueLambda,
)
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> } val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
val resetBatteryOptimizationStateResult = lambdaRecorder<Unit> { } val resetBatteryOptimizationStateResult = lambdaRecorder<Unit> { }
val pushService = FakePushService( val pushService = FakePushService(
@ -59,7 +54,6 @@ class DefaultClearCacheUseCaseTest {
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
defaultCacheService = defaultCacheService, defaultCacheService = defaultCacheService,
okHttpClient = { OkHttpClient.Builder().build() }, okHttpClient = { OkHttpClient.Builder().build() },
ftueService = ftueService,
pushService = pushService, pushService = pushService,
seenInvitesStore = seenInvitesStore, seenInvitesStore = seenInvitesStore,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
@ -67,7 +61,6 @@ class DefaultClearCacheUseCaseTest {
defaultCacheService.clearedCacheEventFlow.test { defaultCacheService.clearedCacheEventFlow.test {
sut.invoke() sut.invoke()
clearCacheLambda.assertions().isCalledOnce() clearCacheLambda.assertions().isCalledOnce()
resetFtueLambda.assertions().isCalledOnce()
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
.with(value(matrixClient.sessionId), value(false)) .with(value(matrixClient.sessionId), value(false))
resetBatteryOptimizationStateResult.assertions().isCalledOnce() resetBatteryOptimizationStateResult.assertions().isCalledOnce()

View file

@ -50,6 +50,8 @@
<string name="screen_room_details_error_loading_notification_settings">"Při načítání nastavení oznámení došlo k chybě."</string> <string name="screen_room_details_error_loading_notification_settings">"Při načítání nastavení oznámení došlo k chybě."</string>
<string name="screen_room_details_error_muting">"Ztišení této místnosti se nezdařilo, zkuste to prosím znovu."</string> <string name="screen_room_details_error_muting">"Ztišení této místnosti se nezdařilo, zkuste to prosím znovu."</string>
<string name="screen_room_details_error_unmuting">"Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."</string> <string name="screen_room_details_error_unmuting">"Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."</string>
<string name="screen_room_details_invite_people_dont_close">"Nezavírejte aplikaci, dokud neskončíte."</string>
<string name="screen_room_details_invite_people_preparing">"Příprava pozvánek…"</string>
<string name="screen_room_details_invite_people_title">"Pozvat přátele"</string> <string name="screen_room_details_invite_people_title">"Pozvat přátele"</string>
<string name="screen_room_details_leave_conversation_title">"Opustit konverzaci"</string> <string name="screen_room_details_leave_conversation_title">"Opustit konverzaci"</string>
<string name="screen_room_details_leave_room_title">"Opustit místnost"</string> <string name="screen_room_details_leave_room_title">"Opustit místnost"</string>

View file

@ -7,7 +7,7 @@
<string name="screen_polls_history_title">"Pleidleisiau"</string> <string name="screen_polls_history_title">"Pleidleisiau"</string>
<string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string> <string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string>
<string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string> <string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string>
<string name="screen_room_change_permissions_delete_messages">"Dileu negeseuon"</string> <string name="screen_room_change_permissions_delete_messages">"Tynnu negeseuon"</string>
<string name="screen_room_change_permissions_everyone">"Pawb"</string> <string name="screen_room_change_permissions_everyone">"Pawb"</string>
<string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string> <string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string>
<string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string> <string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string>
@ -22,13 +22,17 @@
<string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string> <string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string> <string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string> <string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string>
<string name="screen_room_change_role_confirm_change_owners_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol."</string>
<string name="screen_room_change_role_confirm_change_owners_title">"Trosglwyddo perchnogaeth?"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string> <string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string> <string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string> <string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string> <string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string>
<string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string> <string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string> <string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string>
<string name="screen_room_change_role_moderators_owner_section_footer">"Mae gan berchnogion freintiau gweinyddwr yn awtomatig."</string>
<string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string> <string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string>
<string name="screen_room_change_role_owners_title">"Dewiswch Berchnogion"</string>
<string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string> <string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string>
<string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string> <string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string>
<string name="screen_room_change_role_section_users">"Aelodau"</string> <string name="screen_room_change_role_section_users">"Aelodau"</string>
@ -46,6 +50,8 @@
<string name="screen_room_details_error_loading_notification_settings">"Digwyddodd gwall wrth lwytho gosodiadau hysbysu."</string> <string name="screen_room_details_error_loading_notification_settings">"Digwyddodd gwall wrth lwytho gosodiadau hysbysu."</string>
<string name="screen_room_details_error_muting">"Wedi methu tewi\'r ystafell hon, ceisiwch eto."</string> <string name="screen_room_details_error_muting">"Wedi methu tewi\'r ystafell hon, ceisiwch eto."</string>
<string name="screen_room_details_error_unmuting">"Wedi methu dad-dewi\'r ystafell hon, ceisiwch eto."</string> <string name="screen_room_details_error_unmuting">"Wedi methu dad-dewi\'r ystafell hon, ceisiwch eto."</string>
<string name="screen_room_details_invite_people_dont_close">"Peidiwch â chau\'r ap nes ei fod wedi gorffen."</string>
<string name="screen_room_details_invite_people_preparing">"Wrthi\'n paratoi gwahoddiadau…"</string>
<string name="screen_room_details_invite_people_title">"Gwahodd pobl"</string> <string name="screen_room_details_invite_people_title">"Gwahodd pobl"</string>
<string name="screen_room_details_leave_conversation_title">"Gadael y sgwrs"</string> <string name="screen_room_details_leave_conversation_title">"Gadael y sgwrs"</string>
<string name="screen_room_details_leave_room_title">"Gadael yr ystafell"</string> <string name="screen_room_details_leave_room_title">"Gadael yr ystafell"</string>
@ -83,6 +89,7 @@
<string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string> <string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string>
<string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string> <string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string>
<string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string> <string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string>
<string name="screen_room_member_list_role_owner">"Perchennog"</string>
<string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string> <string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string>
<string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string> <string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string>
<string name="screen_room_notification_settings_allow_custom">"Caniatáu gosodiad personol"</string> <string name="screen_room_notification_settings_allow_custom">"Caniatáu gosodiad personol"</string>
@ -100,12 +107,14 @@
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Crybwylliadau ac Allweddeiriau\'n unig"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"Crybwylliadau ac Allweddeiriau\'n unig"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"Yn yr ystafell hon, rhowch wybod i mi am"</string> <string name="screen_room_notification_settings_room_custom_settings_title">"Yn yr ystafell hon, rhowch wybod i mi am"</string>
<string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string> <string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string>
<string name="screen_room_roles_and_permissions_admins_and_owners">"Gweinyddwyr a pherchnogion"</string>
<string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string> <string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string>
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string> <string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string>
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string> <string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string>
<string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string> <string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string>
<string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string> <string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string>
<string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string> <string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string>
<string name="screen_room_roles_and_permissions_owners">"Perchnogion"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string> <string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string>
<string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string> <string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string> <string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string>

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_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients can unlock them."</string>
</resources>

View file

@ -21,12 +21,12 @@
<string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string> <string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string>
<string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string> <string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string> <string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Lisää ylläpitäjä?"</string> <string name="screen_room_change_role_confirm_add_admin_title">"Lisätäänkö ylläpitäjä?"</string>
<string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string> <string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string>
<string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string> <string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string> <string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string> <string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Alenna itsesi?"</string> <string name="screen_room_change_role_confirm_demote_self_title">"Haluatko alentaa itsesi?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string> <string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string> <string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string> <string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string>
@ -37,7 +37,7 @@
<string name="screen_room_change_role_section_moderators">"Valvojat"</string> <string name="screen_room_change_role_section_moderators">"Valvojat"</string>
<string name="screen_room_change_role_section_users">"Jäsenet"</string> <string name="screen_room_change_role_section_users">"Jäsenet"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string> <string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string>
<string name="screen_room_change_role_unsaved_changes_title">"Tallenna muutokset?"</string> <string name="screen_room_change_role_unsaved_changes_title">"Tallennetaanko muutokset?"</string>
<string name="screen_room_details_add_topic_title">"Lisää aihe"</string> <string name="screen_room_details_add_topic_title">"Lisää aihe"</string>
<string name="screen_room_details_badge_encrypted">"Salattu"</string> <string name="screen_room_details_badge_encrypted">"Salattu"</string>
<string name="screen_room_details_badge_not_encrypted">"Ei salattu"</string> <string name="screen_room_details_badge_not_encrypted">"Ei salattu"</string>
@ -50,6 +50,8 @@
<string name="screen_room_details_error_loading_notification_settings">"Ilmoitusasetuksia ladattaessa tapahtui virhe."</string> <string name="screen_room_details_error_loading_notification_settings">"Ilmoitusasetuksia ladattaessa tapahtui virhe."</string>
<string name="screen_room_details_error_muting">"Tämän huoneen mykistäminen epäonnistui, yritä uudelleen."</string> <string name="screen_room_details_error_muting">"Tämän huoneen mykistäminen epäonnistui, yritä uudelleen."</string>
<string name="screen_room_details_error_unmuting">"Tämän huoneen mykistyksen poistaminen epäonnistui, yritä uudelleen."</string> <string name="screen_room_details_error_unmuting">"Tämän huoneen mykistyksen poistaminen epäonnistui, yritä uudelleen."</string>
<string name="screen_room_details_invite_people_dont_close">"Älä sulje sovellusta ennen kuin se on valmis."</string>
<string name="screen_room_details_invite_people_preparing">"Valmistellaan kutsuja…"</string>
<string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string> <string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string>
<string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string> <string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string>
<string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string> <string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string>

View file

@ -50,6 +50,7 @@
<string name="screen_room_details_error_loading_notification_settings">"Une erreur sest produite lors du chargement des paramètres de notification."</string> <string name="screen_room_details_error_loading_notification_settings">"Une erreur sest produite lors du chargement des paramètres de notification."</string>
<string name="screen_room_details_error_muting">"Échec de la mise en sourdine de ce salon, veuillez réessayer."</string> <string name="screen_room_details_error_muting">"Échec de la mise en sourdine de ce salon, veuillez réessayer."</string>
<string name="screen_room_details_error_unmuting">"Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer."</string> <string name="screen_room_details_error_unmuting">"Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer."</string>
<string name="screen_room_details_invite_people_preparing">"Préparation des invitations…"</string>
<string name="screen_room_details_invite_people_title">"Inviter des amis"</string> <string name="screen_room_details_invite_people_title">"Inviter des amis"</string>
<string name="screen_room_details_leave_conversation_title">"Quitter la discussion"</string> <string name="screen_room_details_leave_conversation_title">"Quitter la discussion"</string>
<string name="screen_room_details_leave_room_title">"Quitter le salon"</string> <string name="screen_room_details_leave_room_title">"Quitter le salon"</string>

View file

@ -50,6 +50,8 @@
<string name="screen_room_details_error_loading_notification_settings">"載入通知設定時發生錯誤。"</string> <string name="screen_room_details_error_loading_notification_settings">"載入通知設定時發生錯誤。"</string>
<string name="screen_room_details_error_muting">"無法關閉聊天室通知,請再試一次。"</string> <string name="screen_room_details_error_muting">"無法關閉聊天室通知,請再試一次。"</string>
<string name="screen_room_details_error_unmuting">"無法開啟聊天室通知,請再試一次。"</string> <string name="screen_room_details_error_unmuting">"無法開啟聊天室通知,請再試一次。"</string>
<string name="screen_room_details_invite_people_dont_close">"完成前請勿關閉應用程式。"</string>
<string name="screen_room_details_invite_people_preparing">"正在準備邀請……"</string>
<string name="screen_room_details_invite_people_title">"邀請夥伴"</string> <string name="screen_room_details_invite_people_title">"邀請夥伴"</string>
<string name="screen_room_details_leave_conversation_title">"離開對話"</string> <string name="screen_room_details_leave_conversation_title">"離開對話"</string>
<string name="screen_room_details_leave_room_title">"離開聊天室"</string> <string name="screen_room_details_leave_room_title">"離開聊天室"</string>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_chat_backup_key_backup_action_disable">"Delete message backup"</string>
<string name="screen_chat_backup_key_backup_description">"Store your account security and messages securely on the server. This will allow you to view your message history on any new devices. %1$s."</string>
<string name="screen_chat_backup_key_backup_title">"Message backup"</string>
<string name="screen_chat_backup_key_storage_disabled_error">"Turn on message backup to set it up."</string>
<string name="screen_chat_backup_key_storage_toggle_description">"Upload messages from this device"</string>
<string name="screen_chat_backup_key_storage_toggle_title">"Allow message backup"</string>
<string name="screen_chat_backup_recovery_action_change">"Change backup password"</string>
<string name="screen_chat_backup_recovery_action_change_description">"Restore your account security and message history with a backup password if you\'ve lost all your existing devices."</string>
<string name="screen_chat_backup_recovery_action_confirm">"Enter backup password"</string>
<string name="screen_chat_backup_recovery_action_confirm_description">"Your message backup is currently out of sync."</string>
<string name="screen_chat_backup_recovery_action_setup">"Set up backup"</string>
<string name="screen_create_new_recovery_key_list_item_3">"When asked to confirm your device, select %1$s"</string>
<string name="screen_create_new_recovery_key_list_item_4">"Follow the instructions to create a new backup password"</string>
<string name="screen_create_new_recovery_key_list_item_5">"Save your new backup password in a password manager or encrypted note"</string>
<string name="screen_encryption_reset_bullet_3">"You will need to confirm all your existing devices and verify contacts again"</string>
<string name="screen_encryption_reset_footer">"Only start fresh if you don\'t have access to another signed-in device and you\'ve lost your backup password."</string>
<string name="screen_encryption_reset_title">"Can\'t confirm? You\'ll need to start fresh."</string>
<string name="screen_key_backup_disable_description">"Deleting message backup will remove your account security and messages from the server and turn off the following security features:"</string>
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off message backup and delete it?"</string>
<string name="screen_recovery_key_change_description">"Get a new backup password if you\'ve lost your existing one. After changing your backup password, your old one will no longer work."</string>
<string name="screen_recovery_key_change_generate_key">"Generate a new backup password"</string>
<string name="screen_recovery_key_change_success">"Backup password changed"</string>
<string name="screen_recovery_key_change_title">"Change backup password?"</string>
<string name="screen_recovery_key_confirm_error_content">"Please try again to confirm access to your message backup."</string>
<string name="screen_recovery_key_confirm_error_title">"Incorrect backup password"</string>
<string name="screen_recovery_key_confirm_key_description">"You might have seen the terms \"recovery key\", \"security key\" or \"security phrase\" instead of \"backup password\". Don\'t worry, this is all the same."</string>
<string name="screen_recovery_key_confirm_success">"Backup password confirmed"</string>
<string name="screen_recovery_key_confirm_title">"Enter your backup password"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Copied backup password"</string>
<string name="screen_recovery_key_save_action">"Save backup password"</string>
<string name="screen_recovery_key_save_description">"Write down this backup password somewhere safe, like a password manager, encrypted note, or a physical safe."</string>
<string name="screen_recovery_key_save_key_description">"Tap to copy backup password"</string>
<string name="screen_recovery_key_save_title">"Save your backup password somewhere safe"</string>
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new backup password after this step."</string>
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your backup password?"</string>
<string name="screen_recovery_key_setup_description">"Your message backup is protected by a backup password. If you need a new backup password after setup, you can recreate it by selecting Change backup password."</string>
<string name="screen_recovery_key_setup_generate_key">"Generate your backup password"</string>
<string name="screen_recovery_key_setup_success">"Backup setup successful"</string>
<string name="screen_recovery_key_setup_title">"Set up backup"</string>
<string name="screen_reset_encryption_confirmation_alert_title">"Are you sure you want to start fresh?"</string>
<string name="screen_reset_encryption_password_subtitle">"Confirm that you want to start fresh."</string>
</resources>

View file

@ -37,7 +37,7 @@
<string name="screen_recovery_key_change_generate_key">"Luo uusi palautusavain"</string> <string name="screen_recovery_key_change_generate_key">"Luo uusi palautusavain"</string>
<string name="screen_recovery_key_change_generate_key_description">"Älä jaa tätä kenenkään kanssa!"</string> <string name="screen_recovery_key_change_generate_key_description">"Älä jaa tätä kenenkään kanssa!"</string>
<string name="screen_recovery_key_change_success">"Palautusavain vaihdettu"</string> <string name="screen_recovery_key_change_success">"Palautusavain vaihdettu"</string>
<string name="screen_recovery_key_change_title">"Vaihda palautusavain?"</string> <string name="screen_recovery_key_change_title">"Vaihdetaanko palautusavain?"</string>
<string name="screen_recovery_key_confirm_create_new_recovery_key">"Luo uusi palautusavain"</string> <string name="screen_recovery_key_confirm_create_new_recovery_key">"Luo uusi palautusavain"</string>
<string name="screen_recovery_key_confirm_description">"Varmista, ettei kukaan näe tätä ruutua!"</string> <string name="screen_recovery_key_confirm_description">"Varmista, ettei kukaan näe tätä ruutua!"</string>
<string name="screen_recovery_key_confirm_error_content">"Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen."</string> <string name="screen_recovery_key_confirm_error_content">"Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen."</string>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new backup password"</string>
<string name="screen_identity_confirmation_subtitle">"Confirm this device to set up secure messaging."</string>
<string name="screen_identity_confirmation_title">"Confirm it\'s you"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Use backup password"</string>
<string name="screen_identity_confirmed_title">"Device confirmed"</string>
<string name="screen_session_verification_complete_user_subtitle">"Now you can trust this user when sending or receiving messages."</string>
<string name="screen_session_verification_enter_recovery_key">"Enter backup password"</string>
<string name="screen_session_verification_request_success_title">"Device confirmed"</string>
<string name="screen_session_verification_use_another_device_title">"Open the app on another confirmed device"</string>
<string name="screen_session_verification_user_responder_subtitle">"For extra security, another user wants to verify you. You\'ll be shown a set of emojis to compare."</string>
</resources>

View file

@ -219,7 +219,7 @@ inject = "javax.inject:javax.inject:1"
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
# Element Call # Element Call
element_call_embedded = "io.element.android:element-call-embedded:0.15.0" element_call_embedded = "io.element.android:element-call-embedded:0.16.0-rc.4"
# Auto services # Auto services
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }

View file

@ -123,7 +123,7 @@ class RustTimeline(
) )
override val forwardPaginationStatus = MutableStateFlow( override val forwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent) Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent)
) )
init { init {
@ -221,7 +221,6 @@ class RustTimeline(
items = items, items = items,
hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad, hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad, hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
timelineMode = mode,
) )
} }
.let { items -> .let { items ->

View file

@ -18,9 +18,8 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
items: List<MatrixTimelineItem>, items: List<MatrixTimelineItem>,
hasMoreToLoadBackward: Boolean, hasMoreToLoadBackward: Boolean,
hasMoreToLoadForward: Boolean, hasMoreToLoadForward: Boolean,
timelineMode: Timeline.Mode,
): List<MatrixTimelineItem> { ): List<MatrixTimelineItem> {
val shouldAddForwardLoadingIndicator = timelineMode is Timeline.Mode.Live && hasMoreToLoadForward && items.isNotEmpty() val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
val currentTimestamp = systemClock.epochMillis() val currentTimestamp = systemClock.epochMillis()
return buildList { return buildList {
if (hasMoreToLoadBackward) { if (hasMoreToLoadBackward) {

View file

@ -24,7 +24,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2), items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true, hasMoreToLoadBackward = true,
hasMoreToLoadForward = false, hasMoreToLoadForward = false,
timelineMode = Timeline.Mode.Live,
) )
assertThat(result).containsExactly( assertThat(result).containsExactly(
MatrixTimelineItem.Virtual( MatrixTimelineItem.Virtual(
@ -47,7 +46,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2), items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = false, hasMoreToLoadBackward = false,
hasMoreToLoadForward = true, hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
) )
assertThat(result).containsExactly( assertThat(result).containsExactly(
messageEvent, messageEvent,
@ -70,7 +68,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2), items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true, hasMoreToLoadBackward = true,
hasMoreToLoadForward = true, hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
) )
assertThat(result).containsExactly( assertThat(result).containsExactly(
MatrixTimelineItem.Virtual( MatrixTimelineItem.Virtual(
@ -100,7 +97,6 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(), items = listOf(),
hasMoreToLoadBackward = true, hasMoreToLoadBackward = true,
hasMoreToLoadForward = true, hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
) )
assertThat(result).containsExactly( assertThat(result).containsExactly(
MatrixTimelineItem.Virtual( MatrixTimelineItem.Virtual(

View file

@ -16,6 +16,4 @@ interface PermissionStateProvider {
suspend fun setPermissionAsked(permission: String, value: Boolean) suspend fun setPermissionAsked(permission: String, value: Boolean)
fun isPermissionAsked(permission: String): Flow<Boolean> fun isPermissionAsked(permission: String): Flow<Boolean>
suspend fun resetPermission(permission: String)
} }

View file

@ -43,5 +43,6 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
} }

View file

@ -40,6 +40,4 @@ class DefaultPermissionStateProvider(
override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value) override suspend fun setPermissionAsked(permission: String, value: Boolean) = permissionsStore.setPermissionAsked(permission, value)
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionsStore.isPermissionAsked(permission) override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionsStore.isPermissionAsked(permission)
override suspend fun resetPermission(permission: String) = permissionsStore.resetPermission(permission)
} }

View file

@ -15,6 +15,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.impl.R import io.element.android.libraries.permissions.impl.R
import io.element.android.libraries.permissions.impl.action.PermissionActions import io.element.android.libraries.permissions.impl.action.PermissionActions
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@ -54,7 +55,10 @@ class NotificationTroubleshootCheckPermissionTest(
override suspend fun reset() = delegate.reset() override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) { override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
// Do not bother about asking the permission inline, just lead the user to the settings // Do not bother about asking the permission inline, just lead the user to the settings
permissionActions.openSettings() permissionActions.openSettings()
} }

View file

@ -8,11 +8,12 @@
package io.element.android.libraries.permissions.impl.troubleshoot package io.element.android.libraries.permissions.impl.troubleshoot
import android.os.Build import android.os.Build
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.impl.action.FakePermissionActions import io.element.android.libraries.permissions.impl.action.FakePermissionActions
import io.element.android.libraries.permissions.test.FakePermissionStateProvider import io.element.android.libraries.permissions.test.FakePermissionStateProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,10 +29,7 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = FakePermissionActions(), permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -47,10 +45,7 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = FakePermissionActions(), permissionActions = FakePermissionActions(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -74,17 +69,14 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = actions, permissionActions = actions,
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix // Quick fix
launch { backgroundScope.launch {
sut.quickFix(this) sut.quickFix(this, FakeNotificationTroubleshootNavigator())
// Run the test again (IRL it will be done thanks to the resuming of the application) // Run the test again (IRL it will be done thanks to the resuming of the application)
sut.run(this) sut.run(this)
} }
@ -109,13 +101,10 @@ class NotificationTroubleshootCheckPermissionTestTest {
permissionActions = actions, permissionActions = actions,
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
} }

View file

@ -15,7 +15,6 @@ class FakePermissionStateProvider(
private var permissionGranted: Boolean = true, private var permissionGranted: Boolean = true,
permissionDenied: Boolean = false, permissionDenied: Boolean = false,
permissionAsked: Boolean = false, permissionAsked: Boolean = false,
private val resetPermissionLambda: (String) -> Unit = {},
) : PermissionStateProvider { ) : PermissionStateProvider {
private val permissionDeniedFlow = MutableStateFlow(permissionDenied) private val permissionDeniedFlow = MutableStateFlow(permissionDenied)
private val permissionAskedFlow = MutableStateFlow(permissionAsked) private val permissionAskedFlow = MutableStateFlow(permissionAsked)
@ -37,10 +36,4 @@ class FakePermissionStateProvider(
} }
override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow override fun isPermissionAsked(permission: String): Flow<Boolean> = permissionAskedFlow
override suspend fun resetPermission(permission: String) {
setPermissionAsked(permission, false)
setPermissionDenied(permission, false)
resetPermissionLambda(permission)
}
} }

View file

@ -72,6 +72,7 @@ dependencies {
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.features.call.test) testImplementation(projects.features.call.test)
testImplementation(projects.features.lockscreen.test) testImplementation(projects.features.lockscreen.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)

View file

@ -44,7 +44,7 @@ class CurrentPushProviderTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} }

View file

@ -0,0 +1,69 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.troubleshoot
import dev.zacsweers.metro.ContributesIntoSet
import dev.zacsweers.metro.Inject
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.toolbox.api.strings.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@ContributesIntoSet(SessionScope::class)
@Inject
class IgnoredUsersTest(
private val matrixClient: MatrixClient,
private val stringProvider: StringProvider,
) : NotificationTroubleshootTest {
override val order = 80
private val delegate = NotificationTroubleshootTestDelegate(
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_title),
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_description),
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
)
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
override suspend fun run(coroutineScope: CoroutineScope) {
delegate.start()
val ignorerUsers = matrixClient.ignoredUsersFlow.value
if (ignorerUsers.isEmpty()) {
delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_result_none),
status = NotificationTroubleshootTestState.Status.Success,
)
} else {
delegate.updateState(
description = stringProvider.getQuantityString(
R.plurals.troubleshoot_notifications_test_blocked_users_result_some,
ignorerUsers.size,
ignorerUsers.size
),
status = NotificationTroubleshootTestState.Status.Failure(
hasQuickFix = true,
isCritical = false,
quickFixButtonString = stringProvider.getString(R.string.troubleshoot_notifications_test_blocked_users_quick_fix),
),
)
}
}
override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
navigator.openIgnoredUsers()
}
override suspend fun reset() = delegate.reset()
}

View file

@ -54,7 +54,7 @@ class NotificationTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} }
@ -81,7 +81,7 @@ class NotificationTest(
notificationDisplayer.dismissDiagnosticNotification() notificationDisplayer.dismissDiagnosticNotification()
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
) )

View file

@ -13,6 +13,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.gateway.PushGatewayFailure import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.R
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@ -56,7 +57,7 @@ class PushLoopbackTest(
val hasQuickFix = pushService.getCurrentPushProvider()?.canRotateToken() == true val hasQuickFix = pushService.getCurrentPushProvider()?.canRotateToken() == true
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1), description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1),
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix) status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)
) )
job.cancel() job.cancel()
return return
@ -64,7 +65,7 @@ class PushLoopbackTest(
Timber.e(e, "Failed to test push") Timber.e(e, "Failed to test push")
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message), description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
job.cancel() job.cancel()
return return
@ -72,7 +73,7 @@ class PushLoopbackTest(
if (!testPushResult) { if (!testPushResult) {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3), description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
job.cancel() job.cancel()
return return
@ -93,13 +94,16 @@ class PushLoopbackTest(
job.cancel() job.cancel()
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4), description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
) )
} }
override suspend fun quickFix(coroutineScope: CoroutineScope) { override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
delegate.start() delegate.start()
pushService.getCurrentPushProvider()?.rotateToken() pushService.getCurrentPushProvider()?.rotateToken()
run(coroutineScope) run(coroutineScope)

View file

@ -48,7 +48,7 @@ class PushProvidersTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} }

View file

@ -54,6 +54,14 @@
<string name="push_distributor_background_sync_android">"Background synchronization"</string> <string name="push_distributor_background_sync_android">"Background synchronization"</string>
<string name="push_distributor_firebase_android">"Google Services"</string> <string name="push_distributor_firebase_android">"Google Services"</string>
<string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string> <string name="push_no_valid_google_play_services_apk_android">"No valid Google Play Services found. Notifications may not work properly."</string>
<string name="troubleshoot_notifications_test_blocked_users_description">"Checking blocked users"</string>
<string name="troubleshoot_notifications_test_blocked_users_quick_fix">"View blocked users"</string>
<string name="troubleshoot_notifications_test_blocked_users_result_none">"No users are blocked."</string>
<plurals name="troubleshoot_notifications_test_blocked_users_result_some">
<item quantity="one">"You blocked %1$d user. You will not receive notifications for this user."</item>
<item quantity="other">"You blocked %1$d users. You will not receive notifications for these users."</item>
</plurals>
<string name="troubleshoot_notifications_test_blocked_users_title">"Blocked users"</string>
<string name="troubleshoot_notifications_test_current_push_provider_description">"Get the name of the current provider."</string> <string name="troubleshoot_notifications_test_current_push_provider_description">"Get the name of the current provider."</string>
<string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string> <string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string>
<string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string> <string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string>

View file

@ -7,12 +7,11 @@
package io.element.android.libraries.push.impl.troubleshoot package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -23,10 +22,7 @@ class CurrentPushProviderTestTest {
getCurrentPushProvider = FakeGetCurrentPushProvider("foo"), getCurrentPushProvider = FakeGetCurrentPushProvider("foo"),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -41,14 +37,11 @@ class CurrentPushProviderTestTest {
getCurrentPushProvider = FakeGetCurrentPushProvider(null), getCurrentPushProvider = FakeGetCurrentPushProvider(null),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
} }

View file

@ -0,0 +1,86 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.troubleshoot
import com.google.common.truth.Truth.assertThat
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.FakeMatrixClient
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
class IgnoredUsersTestTest {
@Test
fun `test IgnoredUsersTest order`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(),
stringProvider = FakeStringProvider(),
)
assertThat(sut.order).isEqualTo(80)
}
@Test
fun `test IgnoredUsersTest quick fix`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(),
stringProvider = FakeStringProvider(),
)
val openIgnoredUsersResult = lambdaRecorder<Unit> {}
val navigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
}
sut.quickFix(
coroutineScope = backgroundScope,
navigator = navigator,
)
openIgnoredUsersResult.assertions().isCalledOnce()
}
@Test
fun `test IgnoredUsersTest with no blocked users`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf())
),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test IgnoredUsersTest with blocked users`() = runTest {
val sut = IgnoredUsersTest(
matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2))
),
stringProvider = FakeStringProvider(),
)
sut.runAndTestState {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem()
val lastStatus = lastItem.status as NotificationTroubleshootTestState.Status.Failure
assertThat(lastStatus.hasQuickFix).isTrue()
assertThat(lastStatus.isCritical).isFalse()
assertThat(lastStatus.quickFixButtonString).isNotNull()
assertThat(lastItem.description).contains("2")
}
}
}

View file

@ -7,14 +7,13 @@
package io.element.android.libraries.push.impl.troubleshoot package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -31,10 +30,7 @@ class NotificationTestTest {
fun `test NotificationTest notification cannot be displayed`() = runTest { fun `test NotificationTest notification cannot be displayed`() = runTest {
fakeNotificationDisplayer.displayDiagnosticNotificationResult = lambdaRecorder { _ -> false } fakeNotificationDisplayer.displayDiagnosticNotificationResult = lambdaRecorder { _ -> false }
val sut = createNotificationTest() val sut = createNotificationTest()
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java) assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java)
@ -44,10 +40,7 @@ class NotificationTestTest {
@Test @Test
fun `test NotificationTest user does not click on notification`() = runTest { fun `test NotificationTest user does not click on notification`() = runTest {
val sut = createNotificationTest() val sut = createNotificationTest()
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)
@ -60,10 +53,7 @@ class NotificationTestTest {
@Test @Test
fun `test NotificationTest user clicks on notification`() = runTest { fun `test NotificationTest user clicks on notification`() = runTest {
val sut = createNotificationTest() val sut = createNotificationTest()
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser)

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.push.impl.troubleshoot package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_FAILURE_REASON import io.element.android.libraries.matrix.test.A_FAILURE_REASON
@ -15,10 +14,11 @@ import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import io.element.android.libraries.push.test.FakePushService import io.element.android.libraries.push.test.FakePushService
import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -32,14 +32,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
} }
} }
@ -56,14 +53,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
} }
@ -89,17 +83,14 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.quickFix(this) sut.quickFix(this, FakeNotificationTroubleshootNavigator())
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
rotateTokenLambda.assertions().isCalledOnce() rotateTokenLambda.assertions().isCalledOnce()
} }
} }
@ -115,14 +106,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
} }
} }
@ -139,14 +127,11 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
assertThat(lastItem.description).contains(A_FAILURE_REASON) assertThat(lastItem.description).contains(A_FAILURE_REASON)
} }
} }
@ -163,10 +148,7 @@ class PushLoopbackTestTest {
clock = FakeSystemClock(), clock = FakeSystemClock(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()

View file

@ -7,12 +7,11 @@
package io.element.android.libraries.push.impl.troubleshoot package io.element.android.libraries.push.impl.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.pushproviders.test.FakePushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -23,14 +22,11 @@ class PushProvidersTestTest {
pushProviders = emptySet(), pushProviders = emptySet(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
} }
@ -45,10 +41,7 @@ class PushProvidersTestTest {
), ),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()

View file

@ -76,5 +76,6 @@ dependencies {
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
} }

View file

@ -51,7 +51,7 @@ class FirebaseAvailabilityTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure),
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} }

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.FirebaseStore import io.element.android.libraries.pushproviders.firebase.FirebaseStore
import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.R import io.element.android.libraries.pushproviders.firebase.R
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@ -62,7 +63,7 @@ class FirebaseTokenTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure),
status = NotificationTroubleshootTestState.Status.Failure(true) status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
) )
} }
} }
@ -71,7 +72,10 @@ class FirebaseTokenTest(
override suspend fun reset() = delegate.reset() override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) { override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
delegate.start() delegate.start()
firebaseTroubleshooter.troubleshoot() firebaseTroubleshooter.troubleshoot()
run(coroutineScope) run(coroutineScope)

View file

@ -7,14 +7,13 @@
package io.element.android.libraries.pushproviders.firebase.troubleshoot package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -25,10 +24,7 @@ class FirebaseAvailabilityTestTest {
isPlayServiceAvailable = FakeIsPlayServiceAvailable(true), isPlayServiceAvailable = FakeIsPlayServiceAvailable(true),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -42,14 +38,11 @@ class FirebaseAvailabilityTestTest {
isPlayServiceAvailable = FakeIsPlayServiceAvailable(false), isPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
} }

View file

@ -7,15 +7,15 @@
package io.element.android.libraries.pushproviders.firebase.troubleshoot package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -27,10 +27,7 @@ class FirebaseTokenTestTest {
firebaseTroubleshooter = FakeFirebaseTroubleshooter(), firebaseTroubleshooter = FakeFirebaseTroubleshooter(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -53,16 +50,13 @@ class FirebaseTokenTestTest {
), ),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix // Quick fix
sut.quickFix(this) sut.quickFix(this, FakeNotificationTroubleshootNavigator())
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
} }
@ -81,13 +75,10 @@ class FirebaseTokenTestTest {
), ),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
} }

View file

@ -51,6 +51,7 @@ dependencies {
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.troubleshoot.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
} }

View file

@ -48,7 +48,7 @@ class UnifiedPushMatrixGatewayTest(
if (config == null) { if (config == null) {
delegate.updateState( delegate.updateState(
description = "No current push provider", description = "No current push provider",
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} else { } else {
val gatewayBaseUrl = config.url.removeSuffix("/_matrix/push/v1/notify") val gatewayBaseUrl = config.url.removeSuffix("/_matrix/push/v1/notify")
@ -65,13 +65,13 @@ class UnifiedPushMatrixGatewayTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = "${config.url} is not a Matrix gateway.", description = "${config.url} is not a Matrix gateway.",
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
delegate.updateState( delegate.updateState(
description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}", description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}",
status = NotificationTroubleshootTestState.Status.Failure(false) status = NotificationTroubleshootTestState.Status.Failure()
) )
} }
} }

View file

@ -13,6 +13,7 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.pushproviders.unifiedpush.R import io.element.android.libraries.pushproviders.unifiedpush.R
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
@ -57,14 +58,17 @@ class UnifiedPushTest(
} else { } else {
delegate.updateState( delegate.updateState(
description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure), description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure),
status = NotificationTroubleshootTestState.Status.Failure(true) status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
) )
} }
} }
override suspend fun reset() = delegate.reset() override suspend fun reset() = delegate.reset()
override suspend fun quickFix(coroutineScope: CoroutineScope) { override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
openDistributorWebPageAction.execute() openDistributorWebPageAction.execute()
} }
} }

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig
@ -18,8 +17,8 @@ import io.element.android.libraries.pushproviders.unifiedpush.matrixDiscoveryRes
import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -31,10 +30,7 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(), currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = matrixDiscoveryResponse, discoveryResponse = matrixDiscoveryResponse,
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -48,14 +44,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = null, currentUserPushConfig = null,
discoveryResponse = matrixDiscoveryResponse, discoveryResponse = matrixDiscoveryResponse,
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
} }
} }
@ -65,14 +58,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(), currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = invalidDiscoveryResponse, discoveryResponse = invalidDiscoveryResponse,
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
// Reset the error // Reset the error
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
@ -85,14 +75,11 @@ class UnifiedPushMatrixGatewayTestTest {
currentUserPushConfig = aCurrentUserPushConfig(), currentUserPushConfig = aCurrentUserPushConfig(),
discoveryResponse = { error("Network error") }, discoveryResponse = { error("Network error") },
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
} }
} }

View file

@ -7,12 +7,13 @@
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.test.runAndTestState
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -30,10 +31,7 @@ class UnifiedPushTestTest {
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(), openDistributorWebPageAction = FakeOpenDistributorWebPageAction(),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
@ -57,17 +55,14 @@ class UnifiedPushTestTest {
), ),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
// Quick fix // Quick fix
launch { backgroundScope.launch {
sut.quickFix(this) sut.quickFix(this, FakeNotificationTroubleshootNavigator())
sut.run(this) sut.run(this)
} }
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
@ -91,14 +86,11 @@ class UnifiedPushTestTest {
), ),
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { sut.runAndTestState {
sut.run(this)
}
sut.state.test {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
val lastItem = awaitItem() val lastItem = awaitItem()
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
sut.reset() sut.reset()
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
} }

View file

@ -22,5 +22,6 @@ interface NotificationTroubleShootEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { interface Callback : Plugin {
fun onDone() fun onDone()
fun openIgnoredUsers()
} }
} }

View file

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

View file

@ -16,7 +16,10 @@ interface NotificationTroubleshootTest {
fun isRelevant(data: TestFilterData): Boolean = true fun isRelevant(data: TestFilterData): Boolean = true
suspend fun run(coroutineScope: CoroutineScope) suspend fun run(coroutineScope: CoroutineScope)
suspend fun reset() suspend fun reset()
suspend fun quickFix(coroutineScope: CoroutineScope) { suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
error("Quick fix not implemented, you need to override this method in your test") error("Quick fix not implemented, you need to override this method in your test")
} }
} }

View file

@ -60,7 +60,7 @@ class NotificationTroubleshootTestDelegate(
if (isSuccess) { if (isSuccess) {
NotificationTroubleshootTestState.Status.Success NotificationTroubleshootTestState.Status.Success
} else { } else {
NotificationTroubleshootTestState.Status.Failure(hasQuickFix) NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)
} }
) )
} }

View file

@ -17,6 +17,10 @@ data class NotificationTroubleshootTestState(
data object InProgress : Status data object InProgress : Status
data object WaitingForUser : Status data object WaitingForUser : Status
data object Success : Status data object Success : Status
data class Failure(val hasQuickFix: Boolean) : Status data class Failure(
val hasQuickFix: Boolean = false,
val isCritical: Boolean = true,
val quickFixButtonString: String? = null,
) : Status
} }
} }

View file

@ -19,6 +19,7 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -26,15 +27,26 @@ import io.element.android.services.analytics.api.ScreenTracker
class TroubleshootNotificationsNode( class TroubleshootNotificationsNode(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: TroubleshootNotificationsPresenter,
private val screenTracker: ScreenTracker, private val screenTracker: ScreenTracker,
) : Node(buildContext, plugins = plugins) { factory: TroubleshootNotificationsPresenter.Factory,
) : Node(buildContext, plugins = plugins),
NotificationTroubleshootNavigator {
private val presenter = factory.create(
navigator = this,
)
private fun onDone() { private fun onDone() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach { plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.onDone() it.onDone()
} }
} }
override fun openIgnoredUsers() {
plugins<NotificationTroubleShootEntryPoint.Callback>().forEach {
it.openIgnoredUsers()
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot) screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot)

View file

@ -12,14 +12,23 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Inject @Inject
class TroubleshootNotificationsPresenter( class TroubleshootNotificationsPresenter(
@Assisted private val navigator: NotificationTroubleshootNavigator,
private val troubleshootTestSuite: TroubleshootTestSuite, private val troubleshootTestSuite: TroubleshootTestSuite,
) : Presenter<TroubleshootNotificationsState> { ) : Presenter<TroubleshootNotificationsState> {
@AssistedFactory
fun interface Factory {
fun create(navigator: NotificationTroubleshootNavigator): TroubleshootNotificationsPresenter
}
@Composable @Composable
override fun present(): TroubleshootNotificationsState { override fun present(): TroubleshootNotificationsState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -34,7 +43,11 @@ class TroubleshootNotificationsPresenter(
troubleshootTestSuite.runTestSuite(this) troubleshootTestSuite.runTestSuite(this)
} }
is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch { is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch {
troubleshootTestSuite.quickFix(event.testIndex, this) troubleshootTestSuite.quickFix(
testIndex = event.testIndex,
coroutineScope = this,
navigator = navigator,
)
} }
TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch { TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch {
troubleshootTestSuite.retryFailedTest(this) troubleshootTestSuite.retryFailedTest(this)

View file

@ -31,8 +31,12 @@ open class TroubleshootNotificationsStateProvider : PreviewParameterProvider<Tro
aTroubleshootNotificationsState( aTroubleshootNotificationsState(
listOf( listOf(
aTroubleshootTestStateSuccess(), aTroubleshootTestStateSuccess(),
aTroubleshootTestStateFailure(
isCritical = false,
hasQuickFix = true,
quickFixButtonString = "Custom quick fix",
),
aTroubleshootTestStateInProgress(), aTroubleshootTestStateInProgress(),
aTroubleshootTestStateIdle(),
) )
), ),
aTroubleshootNotificationsState( aTroubleshootNotificationsState(
@ -106,5 +110,14 @@ fun aTroubleshootTestStateWaitingForUser() =
fun aTroubleshootTestStateSuccess() = fun aTroubleshootTestStateSuccess() =
aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Success) aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Success)
fun aTroubleshootTestStateFailure(hasQuickFix: Boolean) = fun aTroubleshootTestStateFailure(
aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)) hasQuickFix: Boolean = false,
isCritical: Boolean = true,
quickFixButtonString: String? = null,
) = aTroubleshootTestState(
status = NotificationTroubleshootTestState.Status.Failure(
hasQuickFix = hasQuickFix,
isCritical = isCritical,
quickFixButtonString = quickFixButtonString,
)
)

View file

@ -64,11 +64,12 @@ private fun ColumnScope.TroubleshootTestView(
testState: NotificationTroubleshootTestState, testState: NotificationTroubleshootTestState,
onQuickFixClick: () -> Unit, onQuickFixClick: () -> Unit,
) { ) {
if ((testState.status as? Status.Idle)?.visible == false) return val status = testState.status
if ((status as? Status.Idle)?.visible == false) return
ListItem( ListItem(
headlineContent = { Text(text = testState.name) }, headlineContent = { Text(text = testState.name) },
supportingContent = { Text(text = testState.description) }, supportingContent = { Text(text = testState.description) },
trailingContent = when (testState.status) { trailingContent = when (status) {
is Status.Idle -> null is Status.Idle -> null
Status.InProgress -> ListItemContent.Custom { Status.InProgress -> ListItemContent.Custom {
CircularProgressIndicator( CircularProgressIndicator(
@ -98,20 +99,19 @@ private fun ColumnScope.TroubleshootTestView(
Icon( Icon(
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
imageVector = CompoundIcons.ErrorSolid(), imageVector = if (status.isCritical) CompoundIcons.ErrorSolid() else CompoundIcons.Warning(),
tint = ElementTheme.colors.textCriticalPrimary tint = ElementTheme.colors.iconCriticalPrimary,
) )
} }
} }
) )
if ((testState.status as? Status.Failure)?.hasQuickFix == true) { if (status is Status.Failure && status.hasQuickFix) {
ListItem( ListItem(
headlineContent = { headlineContent = { },
},
trailingContent = ListItemContent.Custom { trailingContent = ListItemContent.Custom {
Button( Button(
text = stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action), text = status.quickFixButtonString ?: stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action),
onClick = onQuickFixClick onClick = onQuickFixClick,
) )
} }
) )

View file

@ -11,6 +11,7 @@ import dev.zacsweers.metro.Inject
import im.vector.app.features.analytics.plan.NotificationTroubleshoot import im.vector.app.features.analytics.plan.NotificationTroubleshoot
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.libraries.troubleshoot.api.test.TestFilterData
@ -90,8 +91,12 @@ class TroubleshootTestSuite(
) )
} }
suspend fun quickFix(testIndex: Int, coroutineScope: CoroutineScope) { suspend fun quickFix(
tests[testIndex].quickFix(coroutineScope) testIndex: Int,
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
tests[testIndex].quickFix(coroutineScope, navigator)
} }
} }
@ -104,7 +109,7 @@ fun List<NotificationTroubleshootTestState>.computeMainState(): AsyncAction<Unit
else -> { else -> {
if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) { if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) {
AsyncAction.ConfirmingNoParams AsyncAction.ConfirmingNoParams
} else if (any { it.status is NotificationTroubleshootTestState.Status.Failure }) { } else if (any { it.status.let { status -> status is NotificationTroubleshootTestState.Status.Failure && status.isCritical } }) {
AsyncAction.Failure(Exception("Some tests failed")) AsyncAction.Failure(Exception("Some tests failed"))
} else { } else {
AsyncAction.Success(Unit) AsyncAction.Success(Unit)

View file

@ -28,12 +28,13 @@ class DefaultNotificationTroubleShootEntryPointTest {
TroubleshootNotificationsNode( TroubleshootNotificationsNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
presenter = createTroubleshootNotificationsPresenter(), factory = { createTroubleshootNotificationsPresenter() },
screenTracker = FakeScreenTracker(), screenTracker = FakeScreenTracker(),
) )
} }
val callback = object : NotificationTroubleShootEntryPoint.Callback { val callback = object : NotificationTroubleShootEntryPoint.Callback {
override fun onDone() = lambdaError() override fun onDone() = lambdaError()
override fun openIgnoredUsers() = lambdaError()
} }
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
.callback(callback) .callback(callback)

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.troubleshoot.impl package io.element.android.libraries.troubleshoot.impl
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -50,7 +51,10 @@ class FakeNotificationTroubleshootTest(
} }
} }
override suspend fun quickFix(coroutineScope: CoroutineScope) { override suspend fun quickFix(
coroutineScope: CoroutineScope,
navigator: NotificationTroubleshootNavigator,
) {
updateState(NotificationTroubleshootTestState.Status.InProgress) updateState(NotificationTroubleshootTestState.Status.InProgress)
quickFixAction()?.let { quickFixAction()?.let {
_state.emit(it) _state.emit(it)

View file

@ -7,15 +7,15 @@
package io.element.android.libraries.troubleshoot.impl package io.element.android.libraries.troubleshoot.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.push.test.FakeGetCurrentPushProvider import io.element.android.libraries.push.test.FakeGetCurrentPushProvider
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -23,9 +23,7 @@ class TroubleshootNotificationsPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createTroubleshootNotificationsPresenter() val presenter = createTroubleshootNotificationsPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.testSuiteState.tests).isEmpty() assertThat(initialState.testSuiteState.tests).isEmpty()
assertThat(initialState.testSuiteState.mainState).isEqualTo(AsyncAction.Uninitialized) assertThat(initialState.testSuiteState.mainState).isEqualTo(AsyncAction.Uninitialized)
@ -40,9 +38,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter( val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite, troubleshootTestSuite = troubleshootTestSuite,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(TroubleshootNotificationsEvents.StartTests) initialState.eventSink(TroubleshootNotificationsEvents.StartTests)
skipItems(1) skipItems(1)
@ -63,9 +59,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter( val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite, troubleshootTestSuite = troubleshootTestSuite,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests)
skipItems(1) skipItems(1)
@ -74,6 +68,80 @@ class TroubleshootNotificationsPresenterTest {
} }
} }
@Test
fun `present - critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true)
)
),
expectedIsCritical = true,
expectedMainState = AsyncAction.Failure::class.java,
)
}
@Test
fun `present - success and critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Success
),
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = true)
),
),
expectedIsCritical = true,
expectedMainState = AsyncAction.Failure::class.java,
)
}
@Test
fun `present - non critical failed test`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.Failure(isCritical = false)
)
),
expectedIsCritical = false,
expectedMainState = AsyncAction.Success::class.java,
)
}
@Test
fun `present - waiting for user`() {
`present - check main state`(
tests = setOf(
FakeNotificationTroubleshootTest(
firstStatus = NotificationTroubleshootTestState.Status.WaitingForUser
)
),
expectedIsCritical = false,
expectedMainState = AsyncAction.ConfirmingNoParams::class.java,
)
}
private fun `present - check main state`(
tests: Set<NotificationTroubleshootTest>,
expectedIsCritical: Boolean,
expectedMainState: Class<out AsyncAction<*>>,
) = runTest {
val troubleshootTestSuite = createTroubleshootTestSuite(
tests = tests
)
val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite,
)
presenter.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.hasFailedTests).isEqualTo(expectedIsCritical)
assertThat(initialState.testSuiteState.mainState).isInstanceOf(expectedMainState)
}
}
@Test @Test
fun `present - quick fix test`() = runTest { fun `present - quick fix test`() = runTest {
val troubleshootTestSuite = createTroubleshootTestSuite( val troubleshootTestSuite = createTroubleshootTestSuite(
@ -86,9 +154,7 @@ class TroubleshootNotificationsPresenterTest {
val presenter = createTroubleshootNotificationsPresenter( val presenter = createTroubleshootNotificationsPresenter(
troubleshootTestSuite = troubleshootTestSuite, troubleshootTestSuite = troubleshootTestSuite,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java) assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java)
@ -111,9 +177,13 @@ private fun createTroubleshootTestSuite(
} }
internal fun createTroubleshootNotificationsPresenter( internal fun createTroubleshootNotificationsPresenter(
navigator: NotificationTroubleshootNavigator = object : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = lambdaError()
},
troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(), troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(),
): TroubleshootNotificationsPresenter { ): TroubleshootNotificationsPresenter {
return TroubleshootNotificationsPresenter( return TroubleshootNotificationsPresenter(
navigator = navigator,
troubleshootTestSuite = troubleshootTestSuite, troubleshootTestSuite = troubleshootTestSuite,
) )
} }

View file

@ -0,0 +1,31 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.troubleshoot.test"
}
dependencies {
implementation(projects.libraries.troubleshoot.api)
implementation(projects.tests.testutils)
implementation(libs.coroutines.test)
implementation(libs.test.core)
implementation(libs.test.turbine)
}
ktlint {
filter {
exclude { element ->
val path = element.file.path
// Exclude this file, that ktlint cannot parse.
path.contains("libraries/troubleshoot/test/src/main/kotlin/io/element/android/libraries/troubleshoot/test/Utils.kt")
}
}
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.troubleshoot.test
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
import io.element.android.tests.testutils.lambda.lambdaError
class FakeNotificationTroubleshootNavigator(
private val openIgnoredUsersResult: () -> Unit = { lambdaError() },
) : NotificationTroubleshootNavigator {
override fun openIgnoredUsers() = openIgnoredUsersResult()
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("UnusedImports")
package io.element.android.libraries.troubleshoot.test
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
context(testScope: TestScope)
suspend fun NotificationTroubleshootTest.runAndTestState(
validate: suspend TurbineTestContext<NotificationTroubleshootTestState>.() -> Unit,
) {
testScope.backgroundScope.launch {
run(this)
}
state.test(validate = validate)
}

View file

@ -44,7 +44,7 @@
<string name="a11y_remove_reaction_with">"Odstranit reakci pomocí %1$s"</string> <string name="a11y_remove_reaction_with">"Odstranit reakci pomocí %1$s"</string>
<string name="a11y_room_avatar">"Avatar místnosti"</string> <string name="a11y_room_avatar">"Avatar místnosti"</string>
<string name="a11y_send_files">"Odeslat soubory"</string> <string name="a11y_send_files">"Odeslat soubory"</string>
<string name="a11y_session_verification_time_limited_action_required">"Vyžaduje se časově omezená akce"</string> <string name="a11y_session_verification_time_limited_action_required">"Vyžaduje se časově omezená akce, na ověření máte jednu minutu"</string>
<string name="a11y_show_password">"Zobrazit heslo"</string> <string name="a11y_show_password">"Zobrazit heslo"</string>
<string name="a11y_start_call">"Zahájit hovor"</string> <string name="a11y_start_call">"Zahájit hovor"</string>
<string name="a11y_tombstoned_room">"Místnost s náhrobkem"</string> <string name="a11y_tombstoned_room">"Místnost s náhrobkem"</string>
@ -107,6 +107,7 @@
<string name="action_leave">"Odejít"</string> <string name="action_leave">"Odejít"</string>
<string name="action_leave_conversation">"Opustit konverzaci"</string> <string name="action_leave_conversation">"Opustit konverzaci"</string>
<string name="action_leave_room">"Opustit místnost"</string> <string name="action_leave_room">"Opustit místnost"</string>
<string name="action_leave_space">"Opustit prostor"</string>
<string name="action_load_more">"Načíst více"</string> <string name="action_load_more">"Načíst více"</string>
<string name="action_manage_account">"Spravovat účet"</string> <string name="action_manage_account">"Spravovat účet"</string>
<string name="action_manage_devices">"Spravovat zařízení"</string> <string name="action_manage_devices">"Spravovat zařízení"</string>
@ -167,6 +168,8 @@
<string name="banner_migrate_to_native_sliding_sync_title">"Upgrade k dispozici"</string> <string name="banner_migrate_to_native_sliding_sync_title">"Upgrade k dispozici"</string>
<string name="common_about">"O aplikaci"</string> <string name="common_about">"O aplikaci"</string>
<string name="common_acceptable_use_policy">"Zásady používání"</string> <string name="common_acceptable_use_policy">"Zásady používání"</string>
<string name="common_add_account">"Přidat účet"</string>
<string name="common_add_another_account">"Přidat další účet"</string>
<string name="common_adding_caption">"Přidání titulku"</string> <string name="common_adding_caption">"Přidání titulku"</string>
<string name="common_advanced_settings">"Pokročilá nastavení"</string> <string name="common_advanced_settings">"Pokročilá nastavení"</string>
<string name="common_an_image">"obrázek"</string> <string name="common_an_image">"obrázek"</string>
@ -184,9 +187,11 @@
<string name="common_creating_room">"Vytváření místnosti…"</string> <string name="common_creating_room">"Vytváření místnosti…"</string>
<string name="common_current_user_canceled_knock">"Žádost zrušena"</string> <string name="common_current_user_canceled_knock">"Žádost zrušena"</string>
<string name="common_current_user_left_room">"Místnost opuštěna"</string> <string name="common_current_user_left_room">"Místnost opuštěna"</string>
<string name="common_current_user_left_space">"Opustit prostor"</string>
<string name="common_current_user_rejected_invite">"Pozvánka odmítnuta"</string> <string name="common_current_user_rejected_invite">"Pozvánka odmítnuta"</string>
<string name="common_dark">"Tmavé"</string> <string name="common_dark">"Tmavé"</string>
<string name="common_decryption_error">"Chyba dešifrování"</string> <string name="common_decryption_error">"Chyba dešifrování"</string>
<string name="common_description">"Popis"</string>
<string name="common_developer_options">"Možnosti pro vývojáře"</string> <string name="common_developer_options">"Možnosti pro vývojáře"</string>
<string name="common_device_id">"ID zařízení"</string> <string name="common_device_id">"ID zařízení"</string>
<string name="common_direct_chat">"Přímý chat"</string> <string name="common_direct_chat">"Přímý chat"</string>
@ -299,14 +304,17 @@ Důvod: %1$s."</string>
<string name="common_search_results">"Výsledky hledání"</string> <string name="common_search_results">"Výsledky hledání"</string>
<string name="common_security">"Zabezpečení"</string> <string name="common_security">"Zabezpečení"</string>
<string name="common_seen_by">"Viděno"</string> <string name="common_seen_by">"Viděno"</string>
<string name="common_select_account">"Vybrat účet"</string>
<string name="common_send_to">"Odeslat do"</string> <string name="common_send_to">"Odeslat do"</string>
<string name="common_sending">"Odesílání…"</string> <string name="common_sending">"Odesílání…"</string>
<string name="common_sending_failed">"Odeslání se nezdařilo"</string> <string name="common_sending_failed">"Odeslání se nezdařilo"</string>
<string name="common_sent">"Odesláno"</string> <string name="common_sent">"Odesláno"</string>
<string name="common_sentence_delimiter">". "</string> <string name="common_sentence_delimiter">". "</string>
<string name="common_server_not_supported">"Server není podporován"</string> <string name="common_server_not_supported">"Server není podporován"</string>
<string name="common_server_unreachable">"Server je nedostupný"</string>
<string name="common_server_url">"URL serveru"</string> <string name="common_server_url">"URL serveru"</string>
<string name="common_settings">"Nastavení"</string> <string name="common_settings">"Nastavení"</string>
<string name="common_share_space">"Sdílet prostor"</string>
<string name="common_shared_location">"Sdílená poloha"</string> <string name="common_shared_location">"Sdílená poloha"</string>
<string name="common_signing_out">"Odhlašování"</string> <string name="common_signing_out">"Odhlašování"</string>
<string name="common_something_went_wrong">"Něco se nepovedlo"</string> <string name="common_something_went_wrong">"Něco se nepovedlo"</string>
@ -382,6 +390,8 @@ Opravdu chcete pokračovat?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Maximální povolená velikost souboru je: %1$s"</string> <string name="dialog_video_quality_selector_subtitle_file_size">"Maximální povolená velikost souboru je: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Vyberte kvalitu videa, které chcete nahrát."</string> <string name="dialog_video_quality_selector_subtitle_no_file_size">"Vyberte kvalitu videa, které chcete nahrát."</string>
<string name="dialog_video_quality_selector_title">"Vyberte kvalitu nahrávání videa"</string> <string name="dialog_video_quality_selector_title">"Vyberte kvalitu nahrávání videa"</string>
<string name="emoji_picker_search_placeholder">"Hledat emotikony"</string>
<string name="error_account_already_logged_in">"Na tomto zařízení jste již přihlášeni jako %1$s."</string>
<string name="error_account_creation_not_possible">"Váš domovský server je třeba upgradovat, aby podporoval službu Matrix Authentication Service a vytváření účtu."</string> <string name="error_account_creation_not_possible">"Váš domovský server je třeba upgradovat, aby podporoval službu Matrix Authentication Service a vytváření účtu."</string>
<string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string> <string name="error_failed_creating_the_permalink">"Vytvoření trvalého odkazu se nezdařilo"</string>
<string name="error_failed_loading_map">"%1$s nemohl načíst mapu. Zkuste to prosím později."</string> <string name="error_failed_loading_map">"%1$s nemohl načíst mapu. Zkuste to prosím později."</string>
@ -409,6 +419,9 @@ Opravdu chcete pokračovat?"</string>
<string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string> <string name="invite_friends_text">"Ahoj, ozvi se mi na %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"%1$s Android"</string> <string name="login_initial_device_name_android">"%1$s Android"</string>
<string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string> <string name="preference_rageshake">"Zatřeste zařízením pro nahlášení chyby"</string>
<string name="screen_bottom_sheet_leave_space_subtitle">"Tím budete také odstraněni ze všech místností v tomto prostoru."</string>
<string name="screen_bottom_sheet_leave_space_subtitle_admin">"Tímto budete také odstraněni ze všech místností v tomto prostoru, včetně těch, jejichž jediným správcem jste:"</string>
<string name="screen_bottom_sheet_leave_space_title">"Opustit %1$s?"</string>
<string name="screen_bug_report_a11y_screenshot">"Snímek obrazovky"</string> <string name="screen_bug_report_a11y_screenshot">"Snímek obrazovky"</string>
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string> <string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
<string name="screen_create_poll_options_section_title">"Možnosti"</string> <string name="screen_create_poll_options_section_title">"Možnosti"</string>

View file

@ -2,6 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_add_reaction">"Ychwanegu adwaith: %1$s"</string> <string name="a11y_add_reaction">"Ychwanegu adwaith: %1$s"</string>
<string name="a11y_avatar">"Afatar"</string> <string name="a11y_avatar">"Afatar"</string>
<string name="a11y_collapse_message_text_field">"Lleihau maes testun neges"</string>
<string name="a11y_delete">"Dileu"</string> <string name="a11y_delete">"Dileu"</string>
<plurals name="a11y_digits_entered"> <plurals name="a11y_digits_entered">
<item quantity="zero">"%1$d nodau wedi eu cynnig"</item> <item quantity="zero">"%1$d nodau wedi eu cynnig"</item>
@ -12,10 +13,13 @@
<item quantity="other">"%1$d nod wedi eu cynnig"</item> <item quantity="other">"%1$d nod wedi eu cynnig"</item>
</plurals> </plurals>
<string name="a11y_edit_avatar">"Golygu afatar"</string> <string name="a11y_edit_avatar">"Golygu afatar"</string>
<string name="a11y_edit_room_address_hint">"Bydd y cyfeiriad llawn yn%1$s"</string>
<string name="a11y_encryption_details">"Manylion amgryptio"</string> <string name="a11y_encryption_details">"Manylion amgryptio"</string>
<string name="a11y_expand_message_text_field">"Ehangu maes testun neges"</string>
<string name="a11y_hide_password">"Cuddio cyfrinair"</string> <string name="a11y_hide_password">"Cuddio cyfrinair"</string>
<string name="a11y_join_call">"Ymuno â galwad"</string> <string name="a11y_join_call">"Ymuno â galwad"</string>
<string name="a11y_jump_to_bottom">"Symud i\'r gwaelod"</string> <string name="a11y_jump_to_bottom">"Symud i\'r gwaelod"</string>
<string name="a11y_move_the_map_to_my_location">"Symud y map i\'m lleoliad"</string>
<string name="a11y_notifications_mentions_only">"Crybwylliadau\'n unig"</string> <string name="a11y_notifications_mentions_only">"Crybwylliadau\'n unig"</string>
<string name="a11y_notifications_muted">"Wedi\'i Dewi"</string> <string name="a11y_notifications_muted">"Wedi\'i Dewi"</string>
<string name="a11y_notifications_new_mentions">"Crybwylliadau newydd"</string> <string name="a11y_notifications_new_mentions">"Crybwylliadau newydd"</string>
@ -46,9 +50,10 @@
<string name="a11y_remove_reaction_with">"Wedi dileu adwaith gyda %1$s"</string> <string name="a11y_remove_reaction_with">"Wedi dileu adwaith gyda %1$s"</string>
<string name="a11y_room_avatar">"Afatar ystafell"</string> <string name="a11y_room_avatar">"Afatar ystafell"</string>
<string name="a11y_send_files">"Anfon ffeiliau"</string> <string name="a11y_send_files">"Anfon ffeiliau"</string>
<string name="a11y_session_verification_time_limited_action_required">"Mae angen gweithredu â chyfyngiad amser"</string> <string name="a11y_session_verification_time_limited_action_required">"Mae angen gweithredu o fewn amser cyfyngedig, mae gennych un funud i wirio"</string>
<string name="a11y_show_password">"Dangos y cyfrinair"</string> <string name="a11y_show_password">"Dangos y cyfrinair"</string>
<string name="a11y_start_call">"Cychwyn galwad"</string> <string name="a11y_start_call">"Cychwyn galwad"</string>
<string name="a11y_tombstoned_room">"Ystafell Tombstoned"</string>
<string name="a11y_user_avatar">"Afatar defnyddiwr"</string> <string name="a11y_user_avatar">"Afatar defnyddiwr"</string>
<string name="a11y_user_menu">"Dewislen defnyddiwr"</string> <string name="a11y_user_menu">"Dewislen defnyddiwr"</string>
<string name="a11y_view_avatar">"Gweld afatar"</string> <string name="a11y_view_avatar">"Gweld afatar"</string>
@ -93,6 +98,7 @@
<string name="action_enable">"Galluogi"</string> <string name="action_enable">"Galluogi"</string>
<string name="action_end_poll">"Gorffen pleidlais"</string> <string name="action_end_poll">"Gorffen pleidlais"</string>
<string name="action_enter_pin">"Rhoi\'r PIN"</string> <string name="action_enter_pin">"Rhoi\'r PIN"</string>
<string name="action_finish">"Gorffen"</string>
<string name="action_forgot_password">"Wedi anghofio\'ch cyfrinair?"</string> <string name="action_forgot_password">"Wedi anghofio\'ch cyfrinair?"</string>
<string name="action_forward">"Ymlaen"</string> <string name="action_forward">"Ymlaen"</string>
<string name="action_go_back">"Mynd nôl"</string> <string name="action_go_back">"Mynd nôl"</string>
@ -107,6 +113,7 @@
<string name="action_leave">"Gadael"</string> <string name="action_leave">"Gadael"</string>
<string name="action_leave_conversation">"Gadael y sgwrs"</string> <string name="action_leave_conversation">"Gadael y sgwrs"</string>
<string name="action_leave_room">"Gadael yr ystafell"</string> <string name="action_leave_room">"Gadael yr ystafell"</string>
<string name="action_leave_space">"Gadael y gofod"</string>
<string name="action_load_more">"Llwytho rhagor"</string> <string name="action_load_more">"Llwytho rhagor"</string>
<string name="action_manage_account">"Rheoli cyfrif"</string> <string name="action_manage_account">"Rheoli cyfrif"</string>
<string name="action_manage_devices">"Rheoli dyfeisiau"</string> <string name="action_manage_devices">"Rheoli dyfeisiau"</string>
@ -124,8 +131,8 @@
<string name="action_react">"Ymateb"</string> <string name="action_react">"Ymateb"</string>
<string name="action_reject">"Gwrthod"</string> <string name="action_reject">"Gwrthod"</string>
<string name="action_remove">"Tynnu"</string> <string name="action_remove">"Tynnu"</string>
<string name="action_remove_caption">"Dileu capsiwn"</string> <string name="action_remove_caption">"Tynnu capsiwn"</string>
<string name="action_remove_message">"Dileu neges"</string> <string name="action_remove_message">"Tynnu neges"</string>
<string name="action_reply">"Ateb"</string> <string name="action_reply">"Ateb"</string>
<string name="action_reply_in_thread">"Ateb mewn edefyn"</string> <string name="action_reply_in_thread">"Ateb mewn edefyn"</string>
<string name="action_report">"Adroddiadau"</string> <string name="action_report">"Adroddiadau"</string>
@ -167,10 +174,14 @@
<string name="banner_migrate_to_native_sliding_sync_title">"Uwchraddiad ar gael"</string> <string name="banner_migrate_to_native_sliding_sync_title">"Uwchraddiad ar gael"</string>
<string name="common_about">"Ynghylch"</string> <string name="common_about">"Ynghylch"</string>
<string name="common_acceptable_use_policy">"Polisi defnydd derbyniol"</string> <string name="common_acceptable_use_policy">"Polisi defnydd derbyniol"</string>
<string name="common_add_account">"Ychwanegu cyfrif"</string>
<string name="common_add_another_account">"Ychwanegu cyfrif arall"</string>
<string name="common_adding_caption">"Ychwanegu capsiwn"</string> <string name="common_adding_caption">"Ychwanegu capsiwn"</string>
<string name="common_advanced_settings">"Gosodiadau uwch"</string> <string name="common_advanced_settings">"Gosodiadau uwch"</string>
<string name="common_an_image">"delwedd"</string> <string name="common_an_image">"delwedd"</string>
<string name="common_analytics">"Dadansoddeg"</string> <string name="common_analytics">"Dadansoddeg"</string>
<string name="common_android_shortcuts_remove_reason_left_room">"Rydych wedi gadael yr ystafell"</string>
<string name="common_android_shortcuts_remove_reason_session_logged_out">"Rydych wedi\'ch allgofnodi o\'r sesiwn"</string>
<string name="common_appearance">"Gwedd"</string> <string name="common_appearance">"Gwedd"</string>
<string name="common_audio">"Sain"</string> <string name="common_audio">"Sain"</string>
<string name="common_blocked_users">"Defnyddwyr wedi\'u rhwystro"</string> <string name="common_blocked_users">"Defnyddwyr wedi\'u rhwystro"</string>
@ -182,9 +193,11 @@
<string name="common_creating_room">"Wrthi\'n creu ystafell…"</string> <string name="common_creating_room">"Wrthi\'n creu ystafell…"</string>
<string name="common_current_user_canceled_knock">"Cais wedi\'i ddiddymu"</string> <string name="common_current_user_canceled_knock">"Cais wedi\'i ddiddymu"</string>
<string name="common_current_user_left_room">"Wedi gadael yr ystafell"</string> <string name="common_current_user_left_room">"Wedi gadael yr ystafell"</string>
<string name="common_current_user_left_space">"Gofod chwith"</string>
<string name="common_current_user_rejected_invite">"Wedi gwrthod y gwahoddiad"</string> <string name="common_current_user_rejected_invite">"Wedi gwrthod y gwahoddiad"</string>
<string name="common_dark">"Tywyll"</string> <string name="common_dark">"Tywyll"</string>
<string name="common_decryption_error">"Gwall dadgryptio"</string> <string name="common_decryption_error">"Gwall dadgryptio"</string>
<string name="common_description">"Disgrifiad"</string>
<string name="common_developer_options">"Dewisiadau datblygwr"</string> <string name="common_developer_options">"Dewisiadau datblygwr"</string>
<string name="common_device_id">"ID dyfais"</string> <string name="common_device_id">"ID dyfais"</string>
<string name="common_direct_chat">"Sgwrs uniongyrchol"</string> <string name="common_direct_chat">"Sgwrs uniongyrchol"</string>
@ -233,12 +246,12 @@ Rheswm: %1$s."</string>
<item quantity="other">"%d arall"</item> <item quantity="other">"%d arall"</item>
</plurals> </plurals>
<plurals name="common_member_count"> <plurals name="common_member_count">
<item quantity="zero">"%1$d aelodau"</item> <item quantity="zero">"%1$d Aelodau"</item>
<item quantity="one">"%1$d aelod"</item> <item quantity="one">"%1$d Aelod"</item>
<item quantity="two">"%1$d aelod"</item> <item quantity="two">"%1$d Aelod"</item>
<item quantity="few">"%1$d aelod"</item> <item quantity="few">"%1$d Aelod"</item>
<item quantity="many">"%1$d aelod"</item> <item quantity="many">"%1$d Aelod"</item>
<item quantity="other">"%1$d aelod"</item> <item quantity="other">"%1$d Aelod"</item>
</plurals> </plurals>
<string name="common_message">"Neges"</string> <string name="common_message">"Neges"</string>
<string name="common_message_actions">"Gweithredoedd neges"</string> <string name="common_message_actions">"Gweithredoedd neges"</string>
@ -272,14 +285,25 @@ Rheswm: %1$s."</string>
<item quantity="many">"%d pleidlais"</item> <item quantity="many">"%d pleidlais"</item>
<item quantity="other">"%d pleidlais"</item> <item quantity="other">"%d pleidlais"</item>
</plurals> </plurals>
<string name="common_preparing">"Yn paratoi…"</string>
<string name="common_privacy_policy">"Polisi preifatrwydd"</string> <string name="common_privacy_policy">"Polisi preifatrwydd"</string>
<string name="common_private_room">"Ystafell breifat"</string> <string name="common_private_room">"Ystafell breifat"</string>
<string name="common_private_space">"Gofod preifat"</string>
<string name="common_public_room">"Ystafell gyhoeddus"</string> <string name="common_public_room">"Ystafell gyhoeddus"</string>
<string name="common_public_space">"Gofod cyhoeddus"</string>
<string name="common_reaction">"Adwaith"</string> <string name="common_reaction">"Adwaith"</string>
<string name="common_reactions">"Adweithiau"</string> <string name="common_reactions">"Adweithiau"</string>
<string name="common_reason">"Rheswm"</string> <string name="common_reason">"Rheswm"</string>
<string name="common_recovery_key">"Allwedd adfer"</string> <string name="common_recovery_key">"Allwedd adfer"</string>
<string name="common_refreshing">"Wrthi\'n adnewyddu…"</string> <string name="common_refreshing">"Wrthi\'n adnewyddu…"</string>
<plurals name="common_replies">
<item quantity="zero">"%1$d atebion"</item>
<item quantity="one">"%1$d ateb"</item>
<item quantity="two">"%1$d ateb"</item>
<item quantity="few">"%1$d ateb"</item>
<item quantity="many">"%1$d ateb"</item>
<item quantity="other">"%1$d ateb"</item>
</plurals>
<string name="common_replying_to">"Yn ymateb i %1$s"</string> <string name="common_replying_to">"Yn ymateb i %1$s"</string>
<string name="common_report_a_bug">"Adrodd ar wall"</string> <string name="common_report_a_bug">"Adrodd ar wall"</string>
<string name="common_report_a_problem">"Adrodd am broblem"</string> <string name="common_report_a_problem">"Adrodd am broblem"</string>
@ -288,6 +312,14 @@ Rheswm: %1$s."</string>
<string name="common_room">"Ystafell"</string> <string name="common_room">"Ystafell"</string>
<string name="common_room_name">"Enw\'r ystafell"</string> <string name="common_room_name">"Enw\'r ystafell"</string>
<string name="common_room_name_placeholder">"e.e. enw eich project"</string> <string name="common_room_name_placeholder">"e.e. enw eich project"</string>
<plurals name="common_rooms">
<item quantity="zero">"%1$d Ystafelloedd"</item>
<item quantity="one">"%1$d Ystafell"</item>
<item quantity="two">"%1$d Ystafell"</item>
<item quantity="few">"%1$d Ystafell"</item>
<item quantity="many">"%1$d Ystafell"</item>
<item quantity="other">"%1$d Ystafell"</item>
</plurals>
<string name="common_saved_changes">"Newidiadau wedi\'u cadw"</string> <string name="common_saved_changes">"Newidiadau wedi\'u cadw"</string>
<string name="common_saving">"Cadw"</string> <string name="common_saving">"Cadw"</string>
<string name="common_screen_lock">"Clo sgrin"</string> <string name="common_screen_lock">"Clo sgrin"</string>
@ -295,18 +327,30 @@ Rheswm: %1$s."</string>
<string name="common_search_results">"Canlyniadau chwilio"</string> <string name="common_search_results">"Canlyniadau chwilio"</string>
<string name="common_security">"Diogelwch"</string> <string name="common_security">"Diogelwch"</string>
<string name="common_seen_by">"Wedi\'i weld gan"</string> <string name="common_seen_by">"Wedi\'i weld gan"</string>
<string name="common_select_account">"Dewis cyfrif"</string>
<string name="common_send_to">"Anfon at"</string> <string name="common_send_to">"Anfon at"</string>
<string name="common_sending">"Yn anfon…"</string> <string name="common_sending">"Yn anfon…"</string>
<string name="common_sending_failed">"Methodd anfon"</string> <string name="common_sending_failed">"Methodd anfon"</string>
<string name="common_sent">"Anfonwyd"</string> <string name="common_sent">"Anfonwyd"</string>
<string name="common_sentence_delimiter">". "</string> <string name="common_sentence_delimiter">". "</string>
<string name="common_server_not_supported">"Nid yw\'r gweinydd yn cael ei gynnal"</string> <string name="common_server_not_supported">"Nid yw\'r gweinydd yn cael ei gynnal"</string>
<string name="common_server_unreachable">"Gweinydd yn anghyraeddadwy"</string>
<string name="common_server_url">"URL gweinydd"</string> <string name="common_server_url">"URL gweinydd"</string>
<string name="common_settings">"Gosodiadau"</string> <string name="common_settings">"Gosodiadau"</string>
<string name="common_share_space">"Rhannu gofod"</string>
<string name="common_shared_location">"Lleoliad yn cael ei rannu"</string> <string name="common_shared_location">"Lleoliad yn cael ei rannu"</string>
<string name="common_signing_out">"Allgofnodi"</string> <string name="common_signing_out">"Allgofnodi"</string>
<string name="common_something_went_wrong">"Aeth rhywbeth o\'i le"</string> <string name="common_something_went_wrong">"Aeth rhywbeth o\'i le"</string>
<string name="common_something_went_wrong_message">"Wedi canfod mater. Ceisiwch eto."</string> <string name="common_something_went_wrong_message">"Wedi canfod mater. Ceisiwch eto."</string>
<string name="common_space">"Gofod"</string>
<plurals name="common_spaces">
<item quantity="zero">"%1$d Gofodau"</item>
<item quantity="one">"%1$d Gofod"</item>
<item quantity="two">"%1$d Ofod"</item>
<item quantity="few">"%1$d Gofod"</item>
<item quantity="many">"%1$d Gofod"</item>
<item quantity="other">"%1$d Gofod"</item>
</plurals>
<string name="common_starting_chat">"Dechrau sgwrs…"</string> <string name="common_starting_chat">"Dechrau sgwrs…"</string>
<string name="common_sticker">"Sticer"</string> <string name="common_sticker">"Sticer"</string>
<string name="common_success">"Llwyddiant"</string> <string name="common_success">"Llwyddiant"</string>
@ -337,6 +381,12 @@ Rheswm: %1$s."</string>
<string name="common_verify_identity">"Gwirio hunaniaeth"</string> <string name="common_verify_identity">"Gwirio hunaniaeth"</string>
<string name="common_verify_user">"Gwirio defnyddiwr"</string> <string name="common_verify_user">"Gwirio defnyddiwr"</string>
<string name="common_video">"Fideo"</string> <string name="common_video">"Fideo"</string>
<string name="common_video_quality_high">"Ansawdd uchel"</string>
<string name="common_video_quality_high_description">"Yr ansawdd gorau ond maint ffeil mwy"</string>
<string name="common_video_quality_low">"Ansawdd isel"</string>
<string name="common_video_quality_low_description">"Y cyflymder llwytho cyflymaf a\'r maint ffeil lleiaf"</string>
<string name="common_video_quality_standard">"Ansawdd safonol"</string>
<string name="common_video_quality_standard_description">"Cydbwysedd ansawdd a chyflymder llwytho"</string>
<string name="common_voice_message">"Neges llais"</string> <string name="common_voice_message">"Neges llais"</string>
<string name="common_waiting">"Yn aros…"</string> <string name="common_waiting">"Yn aros…"</string>
<string name="common_waiting_for_decryption_key">"Yn aros am y neges hon"</string> <string name="common_waiting_for_decryption_key">"Yn aros am y neges hon"</string>
@ -351,6 +401,10 @@ Rheswm: %1$s."</string>
Ydych chi\'n siŵr eich bod am barhau?"</string> Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="dialog_confirm_link_title">"Gwnewch yn siŵr fod y ddolen hon yn iawn"</string> <string name="dialog_confirm_link_title">"Gwnewch yn siŵr fod y ddolen hon yn iawn"</string>
<string name="dialog_default_video_quality_selector_subtitle">"Dewiswch ansawdd rhagosodedig y fideos rydych chi\'n eu llwytho."</string>
<string name="dialog_default_video_quality_selector_title">"Ansawdd llwytho fideo"</string>
<string name="dialog_file_too_large_to_upload_subtitle">"Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw:%1$s"</string>
<string name="dialog_file_too_large_to_upload_title">"Mae maint y ffeil yn rhy fawr i\'w llwytho"</string>
<string name="dialog_room_reported">"Adroddwyd am yr ystafell"</string> <string name="dialog_room_reported">"Adroddwyd am yr ystafell"</string>
<string name="dialog_room_reported_and_left">"Adroddwyd a gadael yr ystafell"</string> <string name="dialog_room_reported_and_left">"Adroddwyd a gadael yr ystafell"</string>
<string name="dialog_title_confirmation">"Cadarnhad"</string> <string name="dialog_title_confirmation">"Cadarnhad"</string>
@ -359,6 +413,11 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="dialog_title_warning">"Rhybudd"</string> <string name="dialog_title_warning">"Rhybudd"</string>
<string name="dialog_unsaved_changes_description_android">"Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?"</string> <string name="dialog_unsaved_changes_description_android">"Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?"</string>
<string name="dialog_unsaved_changes_title">"Cadw\'r newidiadau?"</string> <string name="dialog_unsaved_changes_title">"Cadw\'r newidiadau?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"Y maint ffeil mwyaf sy\'n cael ei ganiatáu yw: %1$s"</string>
<string name="dialog_video_quality_selector_subtitle_no_file_size">"Dewiswch ansawdd y fideo rydych chi am ei llwytho."</string>
<string name="dialog_video_quality_selector_title">"Dewiswch ansawdd llwytho fideo"</string>
<string name="emoji_picker_search_placeholder">"Chwilio emojis"</string>
<string name="error_account_already_logged_in">"Rydych chi eisoes wedi mewngofnodi ar y ddyfais hon fel %1$s ."</string>
<string name="error_account_creation_not_possible">"Mae angen uwchraddio eich gweinydd cartref i gefnogi Gwasanaeth Dilysu Matrix a chreu cyfrif."</string> <string name="error_account_creation_not_possible">"Mae angen uwchraddio eich gweinydd cartref i gefnogi Gwasanaeth Dilysu Matrix a chreu cyfrif."</string>
<string name="error_failed_creating_the_permalink">"Wedi methu creu\'r ddolen barhaol"</string> <string name="error_failed_creating_the_permalink">"Wedi methu creu\'r ddolen barhaol"</string>
<string name="error_failed_loading_map">"Methodd %1$s â llwytho\'r map. Ceisiwch eto yn nes ymlaen."</string> <string name="error_failed_loading_map">"Methodd %1$s â llwytho\'r map. Ceisiwch eto yn nes ymlaen."</string>
@ -386,6 +445,10 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="invite_friends_text">"Hei, siaradwch â mi ar %1$s: %2$s"</string> <string name="invite_friends_text">"Hei, siaradwch â mi ar %1$s: %2$s"</string>
<string name="login_initial_device_name_android">"Android %1$s"</string> <string name="login_initial_device_name_android">"Android %1$s"</string>
<string name="preference_rageshake">"Rageshake i adrodd gwall"</string> <string name="preference_rageshake">"Rageshake i adrodd gwall"</string>
<string name="screen_bottom_sheet_leave_space_subtitle">"Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn."</string>
<string name="screen_bottom_sheet_leave_space_subtitle_admin">"Bydd hyn hefyd yn eich tynnu o bob ystafell yn y gofod hwn, gan gynnwys y rhai rydych chi\'n unig weinyddwr ar eu cyfer:"</string>
<string name="screen_bottom_sheet_leave_space_title">"Gadael %1$s ?"</string>
<string name="screen_bug_report_a11y_screenshot">"Llun sgrin"</string>
<string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string> <string name="screen_create_poll_option_accessibility_label">"%1$s: %2$s"</string>
<string name="screen_create_poll_options_section_title">"Dewisiadau"</string> <string name="screen_create_poll_options_section_title">"Dewisiadau"</string>
<string name="screen_create_poll_remove_accessibility_label">"Tynnu %1$s"</string> <string name="screen_create_poll_remove_accessibility_label">"Tynnu %1$s"</string>
@ -412,6 +475,7 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="screen_resolve_send_failure_unsigned_device_title">"Dyw eich neges heb ei hanfon oherwydd nid yw %1$s wedi gwirio pob dyfais"</string> <string name="screen_resolve_send_failure_unsigned_device_title">"Dyw eich neges heb ei hanfon oherwydd nid yw %1$s wedi gwirio pob dyfais"</string>
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"Mae un neu fwy o\'ch dyfeisiau heb eu gwirio. Gallwch anfon y neges beth bynnag, neu gallwch ei diddymu am y tro a cheisio eto yn nes ymlaen ar ôl i chi ddilysu eich holl ddyfeisiau."</string> <string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"Mae un neu fwy o\'ch dyfeisiau heb eu gwirio. Gallwch anfon y neges beth bynnag, neu gallwch ei diddymu am y tro a cheisio eto yn nes ymlaen ar ôl i chi ddilysu eich holl ddyfeisiau."</string>
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Nid yw eich neges wedi\'i hanfon oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau"</string> <string name="screen_resolve_send_failure_you_unsigned_device_title">"Nid yw eich neges wedi\'i hanfon oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau"</string>
<string name="screen_room_change_role_administrators_or_owners_title">"Golygu Gweinyddwyr neu Berchnogion"</string>
<string name="screen_room_error_failed_processing_media">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string> <string name="screen_room_error_failed_processing_media">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
<string name="screen_room_error_failed_retrieving_user_details">"Methu â nôl manylion defnyddiwr"</string> <string name="screen_room_error_failed_retrieving_user_details">"Methu â nôl manylion defnyddiwr"</string>
<string name="screen_room_event_pill">"Neges yn %1$s"</string> <string name="screen_room_event_pill">"Neges yn %1$s"</string>
@ -429,6 +493,9 @@ Ydych chi\'n siŵr eich bod am barhau?"</string>
<string name="screen_share_open_google_maps">"Agor yn Google Maps"</string> <string name="screen_share_open_google_maps">"Agor yn Google Maps"</string>
<string name="screen_share_open_osm_maps">"Agor yn OpenStreetMap"</string> <string name="screen_share_open_osm_maps">"Agor yn OpenStreetMap"</string>
<string name="screen_share_this_location_action">"Rhannu\'r lleoliad hwn"</string> <string name="screen_share_this_location_action">"Rhannu\'r lleoliad hwn"</string>
<string name="screen_space_list_description">"Gofodau rydych wedi\'u creu neu wedi ymuno â nhw."</string>
<string name="screen_space_list_details">"%1$s • %2$s"</string>
<string name="screen_space_list_title">"Gofodau"</string>
<string name="screen_timeline_item_menu_send_failure_changed_identity">"Heb anfon y neges oherwydd bod hunaniaeth wedi \'i ddilysu %1$s wedi\'i ailosod."</string> <string name="screen_timeline_item_menu_send_failure_changed_identity">"Heb anfon y neges oherwydd bod hunaniaeth wedi \'i ddilysu %1$s wedi\'i ailosod."</string>
<string name="screen_timeline_item_menu_send_failure_unsigned_device">"Heb anfon y neges oherwydd nid yw %1$s wedi gwirio pob dyfais."</string> <string name="screen_timeline_item_menu_send_failure_unsigned_device">"Heb anfon y neges oherwydd nid yw %1$s wedi gwirio pob dyfais."</string>
<string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Heb anfon y neges oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau."</string> <string name="screen_timeline_item_menu_send_failure_you_unsigned_device">"Heb anfon y neges oherwydd nad ydych wedi gwirio un neu fwy o\'ch dyfeisiau."</string>

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