Merge branch 'develop' into renovate/org.maplibre.gl-android-sdk-11.x

This commit is contained in:
ganfra 2024-05-29 15:43:25 +02:00 committed by GitHub
commit 232c8de702
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1249 changed files with 18041 additions and 8127 deletions

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"გააზიარეთ ანონიმური გამოყენების მონაცემები, რათა დაგვეხმაროთ პრობლემების იდენტიფიცირებაში."</string>
<string name="screen_analytics_settings_read_terms">"შეგიძლიათ, წაიკითხოთ ჩვენი ყველა პირობა %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"აქ"</string>
<string name="screen_analytics_settings_share_data">"გააზიარეთ ანალიტიკური მონაცემები"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Podes ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_settings_share_data">"Partilhar dados de utilização"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"共享匿名使用数据以帮助我们排查问题。"</string>
<string name="screen_analytics_settings_read_terms">"您可以阅读我们的所有条款 %1$s。"</string>
<string name="screen_analytics_settings_read_terms_content_link">"此处"</string>
<string name="screen_analytics_settings_share_data">"共享分析数据"</string>
</resources>

View file

@ -63,15 +63,15 @@ fun AnalyticsOptInView(
) {
val eventSink = state.eventSink
fun onTermsAccepted() {
fun onAcceptTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
}
fun onTermsDeclined() {
fun onDeclineTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
}
BackHandler(onBack = ::onTermsDeclined)
BackHandler(onBack = ::onDeclineTerms)
HeaderFooterPage(
modifier = modifier
.fillMaxSize()
@ -82,8 +82,8 @@ fun AnalyticsOptInView(
content = { AnalyticsOptInContent() },
footer = {
AnalyticsOptInFooter(
onTermsAccepted = ::onTermsAccepted,
onTermsDeclined = ::onTermsDeclined,
onAcceptTerms = ::onAcceptTerms,
onDeclineTerms = ::onDeclineTerms,
)
}
)
@ -165,19 +165,19 @@ private fun AnalyticsOptInContent() {
@Composable
private fun AnalyticsOptInFooter(
onTermsAccepted: () -> Unit,
onTermsDeclined: () -> Unit,
onAcceptTerms: () -> Unit,
onDeclineTerms: () -> Unit,
) {
ButtonColumnMolecule {
Button(
text = stringResource(id = CommonStrings.action_ok),
onClick = onTermsAccepted,
onClick = onAcceptTerms,
modifier = Modifier.fillMaxWidth(),
)
TextButton(
text = stringResource(id = CommonStrings.action_not_now),
size = ButtonSize.Medium,
onClick = onTermsDeclined,
onClick = onDeclineTerms,
modifier = Modifier.fillMaxWidth(),
)
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"ჩვენ არ ჩავწერთ და არ დავაფიქსირებთ პერსონალურ მონაცემებს"</string>
<string name="screen_analytics_prompt_help_us_improve">"გააზიარეთ ანონიმური გამოყენების მონაცემები, რათა დაგვეხმაროთ პრობლემების იდენტიფიცირებაში."</string>
<string name="screen_analytics_prompt_read_terms">"შეგიძლიათ, წაიკითხოთ ჩვენი ყველა პირობა %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"აქ"</string>
<string name="screen_analytics_prompt_settings">"ამის გამორთვა ნებისმიერ დროს შეგიძლიათ"</string>
<string name="screen_analytics_prompt_third_party_sharing">"თქვენს მონაცემებს მესამე პირს არ გადავცემთ"</string>
<string name="screen_analytics_prompt_title">"დაგვეხმარეთ, გავაუმჯობესოთ %1$s"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Não recolheremos ou analisaremos quaisquer dados pessoais"</string>
<string name="screen_analytics_prompt_help_us_improve">"Partilhe dados de utilização anónimos para nos ajudar a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Podes ler todos os nossos termos %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aqui"</string>
<string name="screen_analytics_prompt_settings">"Podes desligar qualquer momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Não partilharemos os teus dados com terceiros"</string>
<string name="screen_analytics_prompt_title">"Ajude a melhorar a %1$s"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"我们不会记录或分析任何个人数据"</string>
<string name="screen_analytics_prompt_help_us_improve">"共享匿名使用数据以帮助我们排查问题。"</string>
<string name="screen_analytics_prompt_read_terms">"您可以阅读我们的所有条款 %1$s。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"此处"</string>
<string name="screen_analytics_prompt_settings">"你可以随时关闭此功能"</string>
<string name="screen_analytics_prompt_third_party_sharing">"我们不会与第三方共享您的数据"</string>
<string name="screen_analytics_prompt_title">"帮助改进 %1$s"</string>
</resources>

View file

@ -72,15 +72,10 @@ class CallForegroundService : Service() {
startForeground(1, notification)
}
@Suppress("DEPRECATION")
override fun onDestroy() {
super.onDestroy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopForeground(STOP_FOREGROUND_REMOVE)
}
override fun onBind(intent: Intent?): IBinder? {

View file

@ -81,12 +81,12 @@ internal fun CallScreenView(
.fillMaxSize(),
url = state.urlState,
userAgent = state.userAgent,
onPermissionsRequested = { request ->
onPermissionsRequest = { request ->
val androidPermissions = mapWebkitPermissions(request.resources)
val callback: RequestPermissionCallback = { request.grant(it) }
requestPermissions(androidPermissions.toTypedArray(), callback)
},
onWebViewCreated = { webView ->
onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
}
@ -98,8 +98,8 @@ internal fun CallScreenView(
private fun CallWebView(
url: AsyncData<String>,
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit,
onPermissionsRequest: (PermissionRequest) -> Unit,
onWebViewCreate: (WebView) -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalInspectionMode.current) {
@ -111,8 +111,8 @@ private fun CallWebView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
onWebViewCreated(this)
setup(userAgent, onPermissionsRequested)
onWebViewCreate(this)
setup(userAgent, onPermissionsRequest)
}
},
update = { webView ->

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"მიმდინარე ზარი"</string>
<string name="call_foreground_service_message_android">"დააწკაპუნეთ ზარში დასაბრუნებლად"</string>
<string name="call_foreground_service_title_android">"☎️ ზარი მიმდინარეობს"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Chamada em curso"</string>
<string name="call_foreground_service_message_android">"Toca para voltar à chamada"</string>
<string name="call_foreground_service_title_android">"☎️ Chamada em curso"</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"通话进行中"</string>
<string name="call_foreground_service_message_android">"点按即可返回通话"</string>
<string name="call_foreground_service_title_android">"☎️ 通话中"</string>
</resources>

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
@ -68,7 +68,7 @@ class CallScreenPresenterTest {
@Test
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest {
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val widgetProvider = FakeCallWidgetProvider(widgetDriver)
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -91,7 +91,7 @@ class CallScreenPresenterTest {
@Test
fun `present - set message interceptor, send and receive messages`() = runTest {
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
@ -119,7 +119,7 @@ class CallScreenPresenterTest {
@Test
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
@ -149,7 +149,7 @@ class CallScreenPresenterTest {
@Test
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver,
@ -178,7 +178,7 @@ class CallScreenPresenterTest {
@Test
fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -201,7 +201,7 @@ class CallScreenPresenterTest {
@Test
fun `present - automatically stops the Matrix client sync on dispose`() = runTest {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeWidgetDriver()
val widgetDriver = FakeMatrixWidgetDriver()
val matrixClient = FakeMatrixClient()
val presenter = createCallScreenPresenter(
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
@ -229,7 +229,7 @@ class CallScreenPresenterTest {
private fun TestScope.createCallScreenPresenter(
callType: CallType,
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver),
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -76,7 +76,7 @@ class DefaultCallWidgetProviderTest {
fun `getWidget - returns a widget driver when all steps are successful`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
@ -89,7 +89,7 @@ class DefaultCallWidgetProviderTest {
fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
givenGetWidgetDriverResult(Result.success(FakeWidgetDriver()))
givenGetWidgetDriverResult(Result.success(FakeMatrixWidgetDriver()))
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)

View file

@ -19,10 +19,10 @@ package io.element.android.features.call.utils
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
class FakeCallWidgetProvider(
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
private val widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(),
private val url: String = "https://call.element.io",
) : CallWidgetProvider {
var getWidgetCalled = false

View file

@ -47,8 +47,8 @@ class AddPeopleNode @AssistedInject constructor(
AddPeopleView(
state = state,
modifier = modifier,
onBackPressed = this::navigateUp,
onNextPressed = this::onContinue,
onBackClick = this::navigateUp,
onNextClick = this::onContinue,
)
}
}

View file

@ -42,8 +42,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AddPeopleView(
state: UserListState,
onBackPressed: () -> Unit,
onNextPressed: () -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -51,14 +51,14 @@ fun AddPeopleView(
topBar = {
AddPeopleViewTopBar(
hasSelectedUsers = state.selectedUsers.isNotEmpty(),
onBackPressed = {
onBackClick = {
if (state.isSearchActive) {
state.eventSink(UserListEvents.OnSearchActiveChanged(false))
} else {
onBackPressed()
onBackClick()
}
},
onNextPressed = onNextPressed,
onNextClick = onNextClick,
)
}
) { padding ->
@ -69,8 +69,8 @@ fun AddPeopleView(
.consumeWindowInsets(padding),
state = state,
showBackButton = false,
onUserSelected = {},
onUserDeselected = {},
onSelectUser = {},
onDeselectUser = {},
)
}
}
@ -79,8 +79,8 @@ fun AddPeopleView(
@Composable
private fun AddPeopleViewTopBar(
hasSelectedUsers: Boolean,
onBackPressed: () -> Unit,
onNextPressed: () -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
TopAppBar(
title = {
@ -89,12 +89,12 @@ private fun AddPeopleViewTopBar(
style = ElementTheme.typography.aliasScreenTitle
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
val textActionResId = if (hasSelectedUsers) CommonStrings.action_next else CommonStrings.action_skip
TextButton(
text = stringResource(id = textActionResId),
onClick = onNextPressed,
onClick = onNextClick,
)
}
)
@ -105,7 +105,7 @@ private fun AddPeopleViewTopBar(
internal fun AddPeopleViewPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreview {
AddPeopleView(
state = state,
onBackPressed = {},
onNextPressed = {},
onBackClick = {},
onNextClick = {},
)
}

View file

@ -41,7 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPrivacyOption(
roomPrivacyItem: RoomPrivacyItem,
onOptionSelected: (RoomPrivacyItem) -> Unit,
onOptionClick: (RoomPrivacyItem) -> Unit,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
) {
@ -50,7 +50,7 @@ fun RoomPrivacyOption(
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { onOptionSelected(roomPrivacyItem) },
onClick = { onOptionClick(roomPrivacyItem) },
role = Role.RadioButton,
)
.padding(8.dp),
@ -98,12 +98,12 @@ internal fun RoomPrivacyOptionPreview() = ElementPreview {
Column {
RoomPrivacyOption(
roomPrivacyItem = aRoomPrivacyItem,
onOptionSelected = {},
onOptionClick = {},
isSelected = true,
)
RoomPrivacyOption(
roomPrivacyItem = aRoomPrivacyItem,
onOptionSelected = {},
onOptionClick = {},
isSelected = false,
)
}

View file

@ -53,11 +53,11 @@ fun SearchUserBar(
showLoader: Boolean,
selectedUsers: ImmutableList<MatrixUser>,
active: Boolean,
isMultiSelectionEnabled: Boolean,
onActiveChanged: (Boolean) -> Unit,
onTextChanged: (String) -> Unit,
onUserSelected: (MatrixUser) -> Unit,
onUserDeselected: (MatrixUser) -> Unit,
isMultiSelectionEnable: Boolean,
onActiveChange: (Boolean) -> Unit,
onTextChange: (String) -> Unit,
onUserSelect: (MatrixUser) -> Unit,
onUserDeselect: (MatrixUser) -> Unit,
modifier: Modifier = Modifier,
showBackButton: Boolean = true,
placeHolderTitle: String = stringResource(CommonStrings.common_search_for_someone),
@ -66,14 +66,14 @@ fun SearchUserBar(
SearchBar(
query = query,
onQueryChange = onTextChanged,
onQueryChange = onTextChange,
active = active,
onActiveChange = onActiveChanged,
onActiveChange = onActiveChange,
modifier = modifier,
placeHolderTitle = placeHolderTitle,
showBackButton = showBackButton,
contentPrefix = {
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
if (isMultiSelectionEnable && active && selectedUsers.isNotEmpty()) {
// We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour
// should change to indicate elevation.
@ -96,7 +96,7 @@ fun SearchUserBar(
contentPadding = PaddingValues(16.dp),
selectedUsers = selectedUsers,
autoScroll = true,
onUserRemoved = onUserDeselected,
onUserRemove = onUserDeselect,
modifier = Modifier.background(appBarContainerColor)
)
}
@ -109,7 +109,7 @@ fun SearchUserBar(
resultState = state,
resultHandler = { users ->
LazyColumn(state = columnState) {
if (isMultiSelectionEnabled) {
if (isMultiSelectionEnable) {
itemsIndexed(users) { index, searchResult ->
SearchMultipleUsersResultItem(
modifier = Modifier.fillMaxWidth(),
@ -117,9 +117,9 @@ fun SearchUserBar(
isUserSelected = selectedUsers.contains(searchResult.matrixUser),
onCheckedChange = { checked ->
if (checked) {
onUserSelected(searchResult.matrixUser)
onUserSelect(searchResult.matrixUser)
} else {
onUserDeselected(searchResult.matrixUser)
onUserDeselect(searchResult.matrixUser)
}
}
)
@ -132,7 +132,7 @@ fun SearchUserBar(
SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(),
searchResult = searchResult,
onClick = { onUserSelected(searchResult.matrixUser) }
onClick = { onUserSelect(searchResult.matrixUser) }
)
if (index < users.lastIndex) {
HorizontalDivider()

View file

@ -44,8 +44,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun UserListView(
state: UserListState,
onUserSelected: (MatrixUser) -> Unit,
onUserDeselected: (MatrixUser) -> Unit,
onSelectUser: (MatrixUser) -> Unit,
onDeselectUser: (MatrixUser) -> Unit,
modifier: Modifier = Modifier,
showBackButton: Boolean = true,
) {
@ -59,17 +59,17 @@ fun UserListView(
selectedUsers = state.selectedUsers,
active = state.isSearchActive,
showLoader = state.showSearchLoader,
isMultiSelectionEnabled = state.isMultiSelectionEnabled,
isMultiSelectionEnable = state.isMultiSelectionEnabled,
showBackButton = showBackButton,
onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
onUserSelected = {
onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) },
onTextChange = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) },
onUserSelect = {
state.eventSink(UserListEvents.AddToSelection(it))
onUserSelected(it)
onSelectUser(it)
},
onUserDeselected = {
onUserDeselect = {
state.eventSink(UserListEvents.RemoveFromSelection(it))
onUserDeselected(it)
onDeselectUser(it)
},
)
@ -78,9 +78,9 @@ fun UserListView(
contentPadding = PaddingValues(16.dp),
selectedUsers = state.selectedUsers,
autoScroll = true,
onUserRemoved = {
onUserRemove = {
state.eventSink(UserListEvents.RemoveFromSelection(it))
onUserDeselected(it)
onDeselectUser(it)
},
)
}
@ -102,10 +102,10 @@ fun UserListView(
onCheckedChange = {
if (isSelected) {
state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser))
onUserDeselected(recentDirectRoom.matrixUser)
onDeselectUser(recentDirectRoom.matrixUser)
} else {
state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser))
onUserSelected(recentDirectRoom.matrixUser)
onSelectUser(recentDirectRoom.matrixUser)
}
},
data = CheckableUserRowData.Resolved(
@ -129,7 +129,7 @@ fun UserListView(
internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview {
UserListView(
state = state,
onUserSelected = {},
onUserDeselected = {},
onSelectUser = {},
onDeselectUser = {},
)
}

View file

@ -50,7 +50,7 @@ class ConfigureRoomNode @AssistedInject constructor(
fun onCreateRoomSuccess(roomId: RoomId)
}
private fun onRoomCreated(roomId: RoomId) {
private fun onCreateRoomSuccess(roomId: RoomId) {
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
}
@ -60,8 +60,8 @@ class ConfigureRoomNode @AssistedInject constructor(
ConfigureRoomView(
state = state,
modifier = modifier,
onBackPressed = this::navigateUp,
onRoomCreated = this::onRoomCreated,
onBackClick = this::navigateUp,
onCreateRoomSuccess = this::onCreateRoomSuccess,
)
}
}

View file

@ -29,12 +29,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
@ -63,27 +61,20 @@ import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ConfigureRoomView(
state: ConfigureRoomState,
onBackPressed: () -> Unit,
onRoomCreated: (RoomId) -> Unit,
onBackClick: () -> Unit,
onCreateRoomSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val itemActionsBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
fun onAvatarClicked() {
fun onAvatarClick() {
focusManager.clearFocus()
coroutineScope.launch {
itemActionsBottomSheetState.show()
}
isAvatarActionsSheetVisible.value = true
}
Scaffold(
@ -91,8 +82,8 @@ fun ConfigureRoomView(
topBar = {
ConfigureRoomToolbar(
isNextActionEnabled = state.isCreateButtonEnabled,
onBackPressed = onBackPressed,
onNextPressed = {
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.CreateRoom(state.config))
},
@ -111,20 +102,20 @@ fun ConfigureRoomView(
modifier = Modifier.padding(horizontal = 16.dp),
avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(),
onAvatarClick = ::onAvatarClicked,
onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
onAvatarClick = ::onAvatarClick,
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
)
RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(),
onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
if (state.config.invites.isNotEmpty()) {
SelectedUsersRowList(
modifier = Modifier.padding(bottom = 16.dp),
contentPadding = PaddingValues(horizontal = 24.dp),
selectedUsers = state.config.invites,
onUserRemoved = {
onUserRemove = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
},
@ -133,7 +124,7 @@ fun ConfigureRoomView(
RoomPrivacyOptions(
modifier = Modifier.padding(bottom = 40.dp),
selected = state.config.privacy,
onOptionSelected = {
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
},
@ -143,8 +134,9 @@ fun ConfigureRoomView(
AvatarActionBottomSheet(
actions = state.avatarActions,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
)
AsyncActionView(
@ -154,7 +146,7 @@ fun ConfigureRoomView(
progressText = stringResource(CommonStrings.common_creating_room),
)
},
onSuccess = { onRoomCreated(it) },
onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom(state.config)) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
@ -169,8 +161,8 @@ fun ConfigureRoomView(
@Composable
private fun ConfigureRoomToolbar(
isNextActionEnabled: Boolean,
onBackPressed: () -> Unit,
onNextPressed: () -> Unit,
onBackClick: () -> Unit,
onNextClick: () -> Unit,
) {
TopAppBar(
title = {
@ -179,12 +171,12 @@ private fun ConfigureRoomToolbar(
style = ElementTheme.typography.aliasScreenTitle,
)
},
navigationIcon = { BackButton(onClick = onBackPressed) },
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
TextButton(
text = stringResource(CommonStrings.action_create),
enabled = isNextActionEnabled,
onClick = onNextPressed,
onClick = onNextClick,
)
}
)
@ -195,7 +187,7 @@ private fun RoomNameWithAvatar(
avatarUri: Uri?,
roomName: String,
onAvatarClick: () -> Unit,
onRoomNameChanged: (String) -> Unit,
onChangeRoomName: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@ -213,7 +205,7 @@ private fun RoomNameWithAvatar(
value = roomName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true,
onValueChange = onRoomNameChanged,
onValueChange = onChangeRoomName,
)
}
}
@ -221,7 +213,7 @@ private fun RoomNameWithAvatar(
@Composable
private fun RoomTopic(
topic: String,
onTopicChanged: (String) -> Unit,
onTopicChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LabelledTextField(
@ -229,7 +221,7 @@ private fun RoomTopic(
label = stringResource(R.string.screen_create_room_topic_label),
value = topic,
placeholder = stringResource(CommonStrings.common_topic_placeholder),
onValueChange = onTopicChanged,
onValueChange = onTopicChange,
maxLines = 3,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
@ -240,7 +232,7 @@ private fun RoomTopic(
@Composable
private fun RoomPrivacyOptions(
selected: RoomPrivacy?,
onOptionSelected: (RoomPrivacyItem) -> Unit,
onOptionClick: (RoomPrivacyItem) -> Unit,
modifier: Modifier = Modifier,
) {
val items = roomPrivacyItems()
@ -249,7 +241,7 @@ private fun RoomPrivacyOptions(
RoomPrivacyOption(
roomPrivacyItem = item,
isSelected = selected == item.privacy,
onOptionSelected = onOptionSelected,
onOptionClick = onOptionClick,
)
}
}
@ -260,7 +252,7 @@ private fun RoomPrivacyOptions(
internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview {
ConfigureRoomView(
state = state,
onBackPressed = {},
onRoomCreated = {},
onBackClick = {},
onCreateRoomSuccess = {},
)
}

View file

@ -68,10 +68,10 @@ class CreateRoomRootNode @AssistedInject constructor(
CreateRoomRootView(
state = state,
modifier = modifier,
onClosePressed = this::navigateUp,
onNewRoomClicked = ::onCreateNewRoom,
onCloseClick = this::navigateUp,
onNewRoomClick = ::onCreateNewRoom,
onOpenDM = ::onStartChatSuccess,
onInviteFriendsClicked = { invitePeople(activity) }
onInviteFriendsClick = { invitePeople(activity) }
)
}

View file

@ -59,17 +59,17 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
fun CreateRoomRootView(
state: CreateRoomRootState,
onClosePressed: () -> Unit,
onNewRoomClicked: () -> Unit,
onCloseClick: () -> Unit,
onNewRoomClick: () -> Unit,
onOpenDM: (RoomId) -> Unit,
onInviteFriendsClicked: () -> Unit,
onInviteFriendsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier.fillMaxWidth(),
topBar = {
if (!state.userListState.isSearchActive) {
CreateRoomRootViewTopBar(onClosePressed = onClosePressed)
CreateRoomRootViewTopBar(onCloseClick = onCloseClick)
}
}
) { paddingValues ->
@ -86,18 +86,18 @@ fun CreateRoomRootView(
state = state.userListState.copy(
recentDirectRooms = persistentListOf(),
),
onUserSelected = {
onSelectUser = {
state.eventSink(CreateRoomRootEvents.StartDM(it))
},
onUserDeselected = { },
onDeselectUser = { },
)
if (!state.userListState.isSearchActive) {
CreateRoomActionButtonsList(
state = state,
onNewRoomClicked = onNewRoomClicked,
onInvitePeopleClicked = onInviteFriendsClicked,
onDmClicked = onOpenDM,
onNewRoomClick = onNewRoomClick,
onInvitePeopleClick = onInviteFriendsClick,
onDmClick = onOpenDM,
)
}
}
@ -125,7 +125,7 @@ fun CreateRoomRootView(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreateRoomRootViewTopBar(
onClosePressed: () -> Unit,
onCloseClick: () -> Unit,
) {
TopAppBar(
title = {
@ -137,7 +137,7 @@ private fun CreateRoomRootViewTopBar(
navigationIcon = {
BackButton(
imageVector = CompoundIcons.Close(),
onClick = onClosePressed,
onClick = onCloseClick,
)
}
)
@ -146,23 +146,23 @@ private fun CreateRoomRootViewTopBar(
@Composable
private fun CreateRoomActionButtonsList(
state: CreateRoomRootState,
onNewRoomClicked: () -> Unit,
onInvitePeopleClicked: () -> Unit,
onDmClicked: (RoomId) -> Unit,
onNewRoomClick: () -> Unit,
onInvitePeopleClick: () -> Unit,
onDmClick: (RoomId) -> Unit,
) {
LazyColumn {
item {
CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_plus,
text = stringResource(id = R.string.screen_create_room_action_create_room),
onClick = onNewRoomClicked,
onClick = onNewRoomClick,
)
}
item {
CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_share_android,
text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName),
onClick = onInvitePeopleClicked,
onClick = onInvitePeopleClick,
)
}
if (state.userListState.recentDirectRooms.isNotEmpty()) {
@ -177,7 +177,7 @@ private fun CreateRoomActionButtonsList(
MatrixUserRow(
modifier = Modifier.clickable(
onClick = {
onDmClicked(recentDirectRoom.roomId)
onDmClick(recentDirectRoom.roomId)
}
),
matrixUser = recentDirectRoom.matrixUser,
@ -222,9 +222,9 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv
ElementPreview {
CreateRoomRootView(
state = state,
onClosePressed = {},
onNewRoomClicked = {},
onCloseClick = {},
onNewRoomClick = {},
onOpenDM = {},
onInviteFriendsClicked = {},
onInviteFriendsClick = {},
)
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"ახალი ოთახი"</string>
<string name="screen_create_room_add_people_title">"ხალხის მოწვევა"</string>
<string name="screen_create_room_error_creating_room">"ოთახის შექმნისას შეცდომა მოხდა"</string>
<string name="screen_create_room_private_option_description">"ამ ოთახში შეტყობინებები დაშიფრულია. შემდგომ დაშიფვრის გამორთვა შეუძლებელია."</string>
<string name="screen_create_room_private_option_title">"კერძო ოთახი (მხოლოდ მოწვევა)"</string>
<string name="screen_create_room_public_option_description">"შეტყობინებები არ არის დაშიფრული და ყველას შეუძლია მათი წაკითხვა. შეგიძლიათ ჩართოთ დაშიფვრა მოგვიანებით."</string>
<string name="screen_create_room_public_option_title">"საჯარო ოთახი (ნებისმიერი)"</string>
<string name="screen_create_room_room_name_label">"ოთახის სახელი"</string>
<string name="screen_create_room_title">"ოთახის შექმნა"</string>
<string name="screen_create_room_topic_label">"თემა (სურვილისამებრ)"</string>
<string name="screen_start_chat_error_starting_chat">"ჩატის დაწყების მცდელობისას შეცდომა მოხდა"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"Nova sala"</string>
<string name="screen_create_room_add_people_title">"Convidar pessoas"</string>
<string name="screen_create_room_error_creating_room">"Ocorreu um erro ao criar a sala"</string>
<string name="screen_create_room_private_option_description">"As mensagens serão cifradas. Uma vez ativada, não é possível desativar a cifragem."</string>
<string name="screen_create_room_private_option_title">"Sala privada (entrada apenas por convite)"</string>
<string name="screen_create_room_public_option_description">"As mensagens não serão cifradas e qualquer um as poderá ler. É possível ativar a cifragem posteriormente."</string>
<string name="screen_create_room_public_option_title">"Sala pública (entrada livre)"</string>
<string name="screen_create_room_room_name_label">"Nome da sala"</string>
<string name="screen_create_room_title">"Criar uma sala"</string>
<string name="screen_create_room_topic_label">"Descrição (opcional)"</string>
<string name="screen_start_chat_error_starting_chat">"Ocorreu um erro ao tentar iniciar uma conversa"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_room_action_create_room">"新聊天室"</string>
<string name="screen_create_room_add_people_title">"邀请朋友"</string>
<string name="screen_create_room_error_creating_room">"创建房间时出错"</string>
<string name="screen_create_room_private_option_description">"此聊天室中的消息已加密。加密无法禁用。"</string>
<string name="screen_create_room_private_option_title">"私人房间(仅限受邀者)"</string>
<string name="screen_create_room_public_option_description">"消息未加密,任何人都可以查看。你可以稍后启用加密。"</string>
<string name="screen_create_room_public_option_title">"公共房间(任何人)"</string>
<string name="screen_create_room_room_name_label">"房间名称"</string>
<string name="screen_create_room_title">"创建房间"</string>
<string name="screen_create_room_topic_label">"主题(可选)"</string>
<string name="screen_start_chat_error_starting_chat">"在开始聊天时发生了错误"</string>
</resources>

View file

@ -47,7 +47,7 @@ class AddPeopleViewTest {
aUserListState(
eventSink = eventsRecorder,
),
onBackPressed = it
onBackClick = it
)
rule.pressBack()
}
@ -75,7 +75,7 @@ class AddPeopleViewTest {
aUserListState(
eventSink = eventsRecorder,
),
onNextPressed = it
onNextClick = it
)
rule.clickOn(CommonStrings.action_skip)
}
@ -85,14 +85,14 @@ class AddPeopleViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAddPeopleView(
state: UserListState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onNextPressed: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(),
onNextClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
AddPeopleView(
state = state,
onBackPressed = onBackPressed,
onNextPressed = onNextPressed,
onBackClick = onBackClick,
onNextClick = onNextClick,
)
}
}

View file

@ -54,7 +54,7 @@ class CreateRoomRootViewTest {
aCreateRoomRootState(
eventSink = eventsRecorder,
),
onClosePressed = it
onCloseClick = it
)
rule.pressBack()
}
@ -68,7 +68,7 @@ class CreateRoomRootViewTest {
aCreateRoomRootState(
eventSink = eventsRecorder,
),
onNewRoomClicked = it
onNewRoomClick = it
)
rule.clickOn(R.string.screen_create_room_action_create_room)
}
@ -84,7 +84,7 @@ class CreateRoomRootViewTest {
applicationName = "test",
eventSink = eventsRecorder,
),
onInviteFriendsClicked = it
onInviteFriendsClick = it
)
val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test")
rule.onNodeWithText(text).performClick()
@ -114,18 +114,18 @@ class CreateRoomRootViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
state: CreateRoomRootState,
onClosePressed: () -> Unit = EnsureNeverCalled(),
onNewRoomClicked: () -> Unit = EnsureNeverCalled(),
onCloseClick: () -> Unit = EnsureNeverCalled(),
onNewRoomClick: () -> Unit = EnsureNeverCalled(),
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onInviteFriendsClicked: () -> Unit = EnsureNeverCalled(),
onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
CreateRoomRootView(
state = state,
onClosePressed = onClosePressed,
onNewRoomClicked = onNewRoomClicked,
onCloseClick = onCloseClick,
onNewRoomClick = onNewRoomClick,
onOpenDM = onOpenDM,
onInviteFriendsClicked = onInviteFriendsClicked,
onInviteFriendsClick = onInviteFriendsClick,
)
}
}

View file

@ -132,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor(
lifecycleScope.launch { moveToNextStep() }
}
}
lockScreenEntryPoint.nodeBuilder(this, buildContext)
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
.callback(callback)
.target(LockScreenEntryPoint.Target.Setup)
.build()
}
}

View file

@ -34,18 +34,18 @@ class WelcomeNode @AssistedInject constructor(
private val buildMeta: BuildMeta,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onContinueClicked()
fun onContinueClick()
}
private fun onContinueClicked() {
plugins.filterIsInstance<Callback>().forEach { it.onContinueClicked() }
private fun onContinueClick() {
plugins.filterIsInstance<Callback>().forEach { it.onContinueClick() }
}
@Composable
override fun View(modifier: Modifier) {
WelcomeView(
applicationName = buildMeta.applicationName,
onContinueClicked = ::onContinueClicked,
onContinueClick = ::onContinueClick,
modifier = modifier
)
}

View file

@ -52,9 +52,9 @@ import kotlinx.collections.immutable.persistentListOf
fun WelcomeView(
applicationName: String,
modifier: Modifier = Modifier,
onContinueClicked: () -> Unit,
onContinueClick: () -> Unit,
) {
BackHandler(onBack = onContinueClicked)
BackHandler(onBack = onContinueClick)
OnBoardingPage(
modifier = modifier
.systemBarsPadding()
@ -90,7 +90,7 @@ fun WelcomeView(
Button(
text = stringResource(CommonStrings.action_continue),
modifier = Modifier.fillMaxWidth(),
onClick = onContinueClicked
onClick = onContinueClick
)
Spacer(modifier = Modifier.height(32.dp))
}
@ -113,6 +113,6 @@ private fun listItems() = persistentListOf(
@Composable
internal fun WelcomeViewPreview() {
ElementPreview {
WelcomeView(applicationName = "Element X", onContinueClicked = {})
WelcomeView(applicationName = "Element X", onContinueClick = {})
}
}

View file

@ -11,11 +11,24 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Злучэнне небяспечнае"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе."</string>
<string name="screen_qr_code_login_device_code_title">"Увядзіце наступны нумар на іншай прыладзе."</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Уваход быў адменены на іншай прыладзе."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Запыт на ўваход скасаваны"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Запыт на іншай прыладзе не быў прыняты."</string>
<string name="screen_qr_code_login_error_declined_title">"Уваход адхілены"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Тэрмін уваходу скончыўся. Калі ласка, паспрабуйце яшчэ раз."</string>
<string name="screen_qr_code_login_error_expired_title">"Уваход у сістэму не быў завершаны своечасова"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Ваша іншая прылада не падтрымлівае ўваход у %s з дапамогай QR-кода.
Паспрабуйце ўвайсці ў сістэму ўручную або адскануйце QR-код з дапамогай іншай прылады."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не падтрымліваецца"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Ваш правайдар уліковага запісу не падтрымлівае %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не падтрымліваецца"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Гатовы да сканавання"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Адкрыйце %1$s на настольнай прыладзе"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Выконвайце паказаныя інструкцыі"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Адсканіруйце QR-код з дапамогай гэтай прылады"</string>
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>

View file

@ -11,11 +11,24 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Připojení není zabezpečené"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Budete požádáni o zadání dvou níže uvedených číslic."</string>
<string name="screen_qr_code_login_device_code_title">"Zadejte níže uvedené číslo na svém dalším zařízení"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Přihlášení bylo na druhém zařízení zrušeno."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Žádost o přihlášení zrušena"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Požadavek na vašem druhém zařízení nebyl přijat."</string>
<string name="screen_qr_code_login_error_declined_title">"Přihlášení odmítnuto"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Platnost přihlášení vypršela. Zkuste to prosím znovu."</string>
<string name="screen_qr_code_login_error_expired_title">"Přihlášení nebylo dokončeno včas"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zařízení nepodporuje přihlášení k %su pomocí QR kódu.
Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízení."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód není podporován"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Váš poskytovatel účtu nepodporuje %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s není podporováno"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Připraveno ke skenování"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Otevřete %1$s na stolním počítači"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Naskenujte QR kód pomocí tohoto zařízení"</string>
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>

View file

@ -11,11 +11,12 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Die Verbindung ist nicht sicher"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben."</string>
<string name="screen_qr_code_login_device_code_title">"Trage die unten angezeigte Zahl auf einem anderen Device ein"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Bereit zum Scannen"</string>
<string name="screen_qr_code_login_initial_state_item_1">"%1$s auf einem Desktop-Gerät öffnen"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Scanne den QR-Code mit diesem Gerät"</string>
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>

View file

@ -11,11 +11,22 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connexion nest pas sécurisée"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil."</string>
<string name="screen_qr_code_login_device_code_title">"Saisissez le nombre ci-dessous sur votre autre appareil"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"La connexion a été annulée sur lautre appareil."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"La demande sur lautre appareil na pas été acceptée."</string>
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un code QR. Essayer de vous connecter manuellement, ou scanner le code QR avec un autre appareil."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"Code QR non supporté"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nest pas supporté"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Prêt à scanner"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Ouvrez %1$s sur un ordinateur"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Scanner le code QR avec cet appareil"</string>
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur lautre appareil."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>

View file

@ -11,11 +11,12 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"A kapcsolat nem biztonságos"</string>
<string name="screen_qr_code_login_device_code_subtitle">"A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén."</string>
<string name="screen_qr_code_login_device_code_title">"Adja meg az alábbi számot a másik eszközén"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Készen áll a beolvasásra"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Nyissa meg az %1$set egy asztali eszközön"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Olvassa be a QR-kódot ezzel az eszközzel"</string>
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>

View file

@ -2,7 +2,33 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Potrai modificare le tue impostazioni in seguito."</string>
<string name="screen_notification_optin_title">"Consenti le notifiche e non perdere mai un messaggio"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Stabilendo la connessione"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E adesso?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Prova ad accedere di nuovo con un codice QR nel caso si sia verificato un problema di rete."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se riscontri lo stesso problema, prova con un altra rete wifi o usa i dati mobili al posto del wifi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se il problema persiste, accedi manualmente"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"La connessione non è sicura"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Ti verrà chiesto di inserire le due cifre mostrate su questo dispositivo."</string>
<string name="screen_qr_code_login_device_code_title">"Inserisci il numero qui sotto sull\'altro dispositivo"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Apri %1$s su un dispositivo desktop"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Clicca sul tuo avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Seleziona %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Collega un nuovo dispositivo\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Segui le istruzioni mostrate"</string>
<string name="screen_qr_code_login_initial_state_title">"Apri %1$s su un altro dispositivo per ottenere il codice QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Usa il codice QR mostrato sull\'altro dispositivo."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Riprova"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Codice QR sbagliato"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Vai alle impostazioni della fotocamera"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Per continuare, è necessario fornire l\'autorizzazione a %1$s per utilizzare la fotocamera del dispositivo."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Consenti l\'accesso alla fotocamera per la scansione del codice QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Scansiona il codice QR"</string>
<string name="screen_qr_code_login_start_over_button">"Ricomincia"</string>
<string name="screen_qr_code_login_unknown_error_description">"Si è verificato un errore inatteso. Riprova."</string>
<string name="screen_qr_code_login_verify_code_loading">"In attesa dell\'altro dispositivo"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Il fornitore dell\'account potrebbe richiedere il seguente codice per verificare l\'accesso."</string>
<string name="screen_qr_code_login_verify_code_title">"Il tuo codice di verifica"</string>
<string name="screen_welcome_bullet_1">"Chiamate, sondaggi, ricerche e altro ancora saranno aggiunti nel corso dell\'anno."</string>
<string name="screen_welcome_bullet_2">"La cronologia dei messaggi per le stanze crittografate non è ancora disponibile."</string>
<string name="screen_welcome_bullet_3">"Ci piacerebbe sentire il tuo parere, facci sapere cosa ne pensi tramite la pagina delle impostazioni."</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="screen_notification_optin_subtitle">"თქვენ შეგიძლიათ შეცვალოთ თქვენი პარამეტრები მოგვიანებით."</string>
<string name="screen_notification_optin_title">"ყველა შეტყობინებაზე შეტყობინებების მიღება"</string>
<string name="screen_welcome_bullet_1">"ზარები, გამოკითხვები, ძიება და სხვა დაემატება ამ წლის ბოლოს."</string>
<string name="screen_welcome_bullet_2">"დაშიფრული ოთახებისთვის შეტყობინებების ისტორია ჯერ არ არის ხელმისაწვდომი."</string>
<string name="screen_welcome_bullet_3">"ჩვენ სიამოვნებით მოვისმინოთ თქვენგან, შეგვატყობინეთ რას ფიქრობთ პარამეტრების გვერდზე."</string>
<string name="screen_welcome_button">"დავიწყოთ!"</string>
<string name="screen_welcome_subtitle">"აი, რა უნდა იცოდეთ:"</string>
<string name="screen_welcome_title">"კეთილი იყოს თქვენი მობრძანება %1$s-ში!"</string>
</resources>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Podes alterar as tuas definições mais tarde."</string>
<string name="screen_notification_optin_title">"Permite as notificações e nunca percas uma mensagem"</string>
<string name="screen_qr_code_login_connecting_subtitle">"A estabelecer uma ligação segura"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Não foi possível estabelecer uma ligação segura com o novo dispositivo. Os teus outros dispositivos continuam seguros, não precisas de te preocupar com eles."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"E agora?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Tenta iniciar sessão novamente com um código QR, caso se trate de um problema de rede"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Se tiveres o mesmo problema, experimenta uma rede Wi-Fi diferente ou utiliza os teus dados móveis."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Se isso não funcionar, inicia sessão manualmente"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Ligação insegura"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo."</string>
<string name="screen_qr_code_login_device_code_title">"Insere o número abaixo no teu dispositivo"</string>
<string name="screen_qr_code_login_error_cancelled_title">"Pedido de início de sessão cancelado"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Pronto para ler"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Abre a %1$s num computador"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Carrega no teu avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Seleciona %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Ligar novo dispositivo”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Lê o código QR com este dispositivo"</string>
<string name="screen_qr_code_login_initial_state_title">"Abre a %1$s noutro dispositivo para obteres o código QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Lê o código QR apresentado no outro dispositivo."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Tentar novamente"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Código QR inválido"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Ir para as configurações da câmara"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Para continuar, tens que dar permissão à %1$s para aceder à câmara do teu dispositivo."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Permitir o acesso à câmara para ler o código QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Ler o código QR"</string>
<string name="screen_qr_code_login_start_over_button">"Começar de novo"</string>
<string name="screen_qr_code_login_unknown_error_description">"Ocorreu um erro inesperado. Tenta novamente."</string>
<string name="screen_qr_code_login_verify_code_loading">"À espera do teu outro dispositivo"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"O teu fornecedor de conta pode pedir o seguinte código para verificar o início de sessão."</string>
<string name="screen_qr_code_login_verify_code_title">"O teu código de verificação"</string>
<string name="screen_welcome_bullet_1">"Chamadas, sondagens, pesquisa e mais funcionalidades vão ser adicionadas ao longo do ano."</string>
<string name="screen_welcome_bullet_2">"O histórico de mensagens em salas cifradas ainda não está disponível."</string>
<string name="screen_welcome_bullet_3">"Gostaríamos de ouvir a tua opinião, diz-nos o que pensas através da página de configurações."</string>
<string name="screen_welcome_button">"Vamos lá!"</string>
<string name="screen_welcome_subtitle">"Eis o que tens de saber:"</string>
<string name="screen_welcome_title">"Bem-vindo à %1$s!"</string>
</resources>

View file

@ -2,7 +2,34 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Puteți modifica setările mai târziu."</string>
<string name="screen_notification_optin_title">"Permiteți notificările și nu pierdeți niciodată un mesaj"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Se stabilește o conexiune securizată"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Și acum?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Încercați să vă conectați din nou cu un cod QR în cazul în care a fost o problemă de rețea."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Dacă întâmpinați aceeași problemă, încercați o altă rețea Wi-Fi sau utilizați datele mobile în loc de Wi-Fi."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Dacă nu funcționează, conectați-vă manual"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Selectați %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Conectați un dispozitiv nou”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Scanați codul QR cu acest dispozitiv"</string>
<string name="screen_qr_code_login_initial_state_title">"Deschideți %1$s pe un alt dispozitiv pentru a obține codul QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Utilizați codul QR afișat pe celălalt dispozitiv."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Încercați din nou"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"Cod QR greșit"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"Mergeți la setările camerei"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"Trebuie să acordați permisiunea ca %1$s să folosească camera dispozitivului pentru a continua."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Permiteți accesul la cameră pentru a scana codul QR"</string>
<string name="screen_qr_code_login_scanning_state_title">"Scanați codul QR"</string>
<string name="screen_qr_code_login_start_over_button">"Începeți din nou"</string>
<string name="screen_qr_code_login_unknown_error_description">"A apărut o eroare neașteptată. Vă rugăm să încercați din nou."</string>
<string name="screen_qr_code_login_verify_code_loading">"În așteptarea celuilalt dispozitiv"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"Furnizorul dumneavoastră de cont poate solicita următorul cod pentru a verifica conectarea."</string>
<string name="screen_qr_code_login_verify_code_title">"Codul dumneavoastră de verificare"</string>
<string name="screen_welcome_bullet_1">"Apelurile, sondajele, căutare și multe altele vor fi adăugate în cursul acestui an."</string>
<string name="screen_welcome_bullet_2">"Istoricul mesajelor pentru camerele criptate nu va fi disponibil în această actualizare."</string>
<string name="screen_welcome_bullet_3">"Ne-ar plăcea să auzim de la dumneavoastră, spuneți-ne ce părere aveți prin intermediul paginii de setări."</string>

View file

@ -2,19 +2,33 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Вы можете изменить настройки позже."</string>
<string name="screen_notification_optin_title">"Разрешите уведомления и никогда не пропустите сообщение"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Установление соединения"</string>
<string name="screen_qr_code_login_connecting_subtitle">"Установление безопасного соединения"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"Не удалось установить безопасное соединение с новым устройством. Существующие устройства по-прежнему в безопасности, и вам не нужно беспокоиться о них."</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Что теперь?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Попробуйте снова войти в систему с помощью QR-кода, если это была сетевая проблема"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"Если это не помогло, войдите вручную"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Соединение не защищено"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Вам будет предложено ввести две цифры, показанные ниже."</string>
<string name="screen_qr_code_login_device_code_title">"Введите номер на своем устройстве"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Вам нужно будет ввести две цифры, показанные на этом устройстве."</string>
<string name="screen_qr_code_login_device_code_title">"Введите показанный номер на своем другом устройстве"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Вход на другом устройстве был отменен."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Запрос на вход отменен"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Запрос не был принят на другом устройстве."</string>
<string name="screen_qr_code_login_error_declined_title">"Вход отклонен"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Срок действия входа истек. Пожалуйста, попробуйте еще раз."</string>
<string name="screen_qr_code_login_error_expired_title">"Вход в систему не был выполнен вовремя"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Другое устройство не поддерживает вход в %s с помощью QR-кода.
Попробуйте войти вручную или отсканируйте QR-код на другом устройстве."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR-код не поддерживается"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Поставщик учетной записи не поддерживает %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s не поддерживается"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Готово к сканированию"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Откройте %1$s на настольном устройстве"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Отсканируйте QR-код с помощью этого устройства"</string>
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>

View file

@ -11,11 +11,24 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Pripojenie nie je bezpečené"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení."</string>
<string name="screen_qr_code_login_device_code_title">"Zadajte nižšie uvedené číslo na vašom druhom zariadení"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"Prihlásenie bolo zrušené na druhom zariadení."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Žiadosť o prihlásenie bola zrušená"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"Žiadosť na vašom druhom zariadení nebola prijatá."</string>
<string name="screen_qr_code_login_error_declined_title">"Prihlásenie bolo odmietnuté"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Platnosť prihlásenia vypršala. Skúste to prosím znova."</string>
<string name="screen_qr_code_login_error_expired_title">"Prihlásenie nebolo včas dokončené"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Vaše druhé zariadenie nepodporuje prihlásenie do aplikácie %s pomocou QR kódu.
Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariadenia."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR kód nie je podporovaný"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Poskytovateľ vášho účtu nepodporuje %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nie je podporovaný"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Pripravené na skenovanie"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Otvorte %1$s na stolnom zariadení"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Postupujte podľa zobrazených pokynov"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Naskenujte QR kód pomocou tohto zariadenia"</string>
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"您可以稍后更改设置。"</string>
<string name="screen_notification_optin_title">"允许通知,绝不错过任何消息"</string>
<string name="screen_qr_code_login_connecting_subtitle">"建立安全连接"</string>
<string name="screen_qr_code_login_connection_note_secure_state_description">"无法与新设备建立安全连接。您现有的设备仍然安全,无需担心。"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"现在怎么办?"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"如果这是网络问题,请尝试使用二维码再次登录"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"如果你遇到同样的问题,请尝试使用不同的 WiFi 网络或使用你的移动数据代替 WiFi"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"如果不起作用,请手动登录"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"连接不安全"</string>
<string name="screen_qr_code_login_device_code_subtitle">"您会被要求输入此设备上显示的两位数。"</string>
<string name="screen_qr_code_login_device_code_title">"在您的其他设备上输入下面的数字"</string>
<string name="screen_qr_code_login_initial_state_item_1">"在桌面设备上打开 %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_2">"点击你的头像"</string>
<string name="screen_qr_code_login_initial_state_item_3">"选择 %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"「连接新设备」"</string>
<string name="screen_qr_code_login_initial_state_item_4">"按照说明进行操作"</string>
<string name="screen_qr_code_login_initial_state_title">"在另一台设备上打开 %1$s 以获取二维码"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"使用其他设备上显示的二维码。"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"再试一次"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"二维码错误"</string>
<string name="screen_qr_code_login_no_camera_permission_button">"转到摄像头设置"</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"您需要授予 %1$s 使用设备摄像头的权限才能继续。"</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"允许摄像头权限以扫描 QR 码"</string>
<string name="screen_qr_code_login_scanning_state_title">"扫描二维码"</string>
<string name="screen_qr_code_login_start_over_button">"重新开始"</string>
<string name="screen_qr_code_login_unknown_error_description">"发生了意外错误。请再试一次。"</string>
<string name="screen_qr_code_login_verify_code_loading">"等着您的其他设备"</string>
<string name="screen_qr_code_login_verify_code_subtitle">"您的账户提供商可能会要求您提供以下代码来验证登录。"</string>
<string name="screen_qr_code_login_verify_code_title">"您的验证码"</string>
<string name="screen_welcome_bullet_1">"今年晚些时候将增加通话、投票、搜索等功能。"</string>
<string name="screen_welcome_bullet_2">"加密房间的消息历史记录尚不可用。"</string>
<string name="screen_welcome_bullet_3">"我们很乐意听取您的意见,请通过设置页面告诉我们您的想法。"</string>
<string name="screen_welcome_button">"开始吧!"</string>
<string name="screen_welcome_subtitle">"以下是您需要了解的内容:"</string>
<string name="screen_welcome_title">"欢迎使用 %1$s"</string>
</resources>

View file

@ -11,11 +11,24 @@
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Youll be asked to enter the two digits shown on this device."</string>
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"The request on your other device was not accepted."</string>
<string name="screen_qr_code_login_error_declined_title">"Sign in declined"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"Sign in expired. Please try again."</string>
<string name="screen_qr_code_login_error_expired_title">"The sign in was not completed in time"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Your other device does not support signing in to %s with a QR code.
Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_qr_code_login_error_linking_not_suported_title">"QR code not supported"</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Your account provider does not support %1$s."</string>
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s not supported"</string>
<string name="screen_qr_code_login_initial_state_button_title">"Ready to scan"</string>
<string name="screen_qr_code_login_initial_state_item_1">"Open %1$s on a desktop device"</string>
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Follow the instructions shown"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Scan the QR code with this device"</string>
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>

View file

@ -16,7 +16,7 @@
package io.element.android.features.ftue.impl.welcome.state
class FakeWelcomeState : WelcomeScreenState {
class FakeWelcomeScreenState : WelcomeScreenState {
private var isWelcomeScreenNeeded = true
override fun isWelcomeScreenNeeded(): Boolean {

View file

@ -25,4 +25,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.services.analytics.api)
}

View file

@ -24,8 +24,8 @@ interface AcceptDeclineInviteView {
@Composable
fun Render(
state: AcceptDeclineInviteState,
onInviteAccepted: (RoomId) -> Unit,
onInviteDeclined: (RoomId) -> Unit,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
modifier: Modifier,
)
}

View file

@ -33,9 +33,8 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.Optional
@ -44,7 +43,7 @@ import kotlin.jvm.optionals.getOrNull
class AcceptDeclineInvitePresenter @Inject constructor(
private val client: MatrixClient,
private val analyticsService: AnalyticsService,
private val joinRoom: JoinRoom,
private val notificationDrawerManager: NotificationDrawerManager,
) : Presenter<AcceptDeclineInviteState> {
@Composable
@ -59,9 +58,11 @@ class AcceptDeclineInvitePresenter @Inject constructor(
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
currentInvite = Optional.of(event.invite)
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
// currentInvite is used to render the decline confirmation dialog
// and to reuse the roomId when the user confirm the rejection of the invitation.
// Just set it to empty here.
currentInvite = Optional.empty()
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
@ -100,14 +101,18 @@ class AcceptDeclineInvitePresenter @Inject constructor(
)
}
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<AsyncAction<RoomId>>) = launch {
private fun CoroutineScope.acceptInvite(
roomId: RoomId,
acceptedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
acceptedAction.runUpdatingState {
client.joinRoom(roomId)
joinRoom(
roomId = roomId,
serverNames = emptyList(),
trigger = JoinedRoom.Trigger.Invite,
)
.onSuccess {
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
client.getRoom(roomId)?.use { room ->
analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite))
}
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
.map { roomId }
}
@ -117,7 +122,7 @@ class AcceptDeclineInvitePresenter @Inject constructor(
suspend {
client.getRoom(roomId)?.use {
it.leave().getOrThrow()
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true)
notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
roomId
}.runCatchingUpdatingState(declinedAction)

View file

@ -36,21 +36,21 @@ import kotlin.jvm.optionals.getOrNull
@Composable
fun AcceptDeclineInviteView(
state: AcceptDeclineInviteState,
onInviteAccepted: (RoomId) -> Unit,
onInviteDeclined: (RoomId) -> Unit,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
AsyncActionView(
async = state.acceptAction,
onSuccess = onInviteAccepted,
onSuccess = onAcceptInvite,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
},
)
AsyncActionView(
async = state.declineAction,
onSuccess = onInviteDeclined,
onSuccess = onDeclineInvite,
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
},
@ -59,10 +59,10 @@ fun AcceptDeclineInviteView(
if (invite != null) {
DeclineConfirmationDialog(
invite = invite,
onConfirmClicked = {
onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
},
onDismissClicked = {
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
}
)
@ -75,8 +75,8 @@ fun AcceptDeclineInviteView(
@Composable
private fun DeclineConfirmationDialog(
invite: InviteData,
onConfirmClicked: () -> Unit,
onDismissClicked: () -> Unit,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentResource = if (invite.isDirect) {
@ -97,8 +97,8 @@ private fun DeclineConfirmationDialog(
title = stringResource(titleResource),
submitText = stringResource(CommonStrings.action_decline),
cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClicked = onConfirmClicked,
onDismiss = onDismissClicked,
onSubmitClick = onConfirmClick,
onDismiss = onDismissClick,
)
}
@ -108,7 +108,7 @@ internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInvit
ElementPreview {
AcceptDeclineInviteView(
state = state,
onInviteAccepted = {},
onInviteDeclined = {},
onAcceptInvite = {},
onDeclineInvite = {},
)
}

View file

@ -30,14 +30,14 @@ class AcceptDeclineInviteViewWrapper @Inject constructor() : AcceptDeclineInvite
@Composable
override fun Render(
state: AcceptDeclineInviteState,
onInviteAccepted: (RoomId) -> Unit,
onInviteDeclined: (RoomId) -> Unit,
onAcceptInvite: (RoomId) -> Unit,
onDeclineInvite: (RoomId) -> Unit,
modifier: Modifier,
) {
AcceptDeclineInviteView(
state = state,
onInviteAccepted = onInviteAccepted,
onInviteDeclined = onInviteDeclined,
onAcceptInvite = onAcceptInvite,
onDeclineInvite = onDeclineInvite,
modifier = modifier
)
}

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_invites_decline_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ მოწვევაზე %1$s-ში?"</string>
<string name="screen_invites_decline_chat_title">"მოწვევაზე უარის თქმა"</string>
<string name="screen_invites_decline_direct_chat_message">"დარწმუნებული ხართ, რომ გსურთ, უარი თქვათ ჩატზე %1$s-თან?"</string>
<string name="screen_invites_decline_direct_chat_title">"ჩატზე უარის თქვა"</string>
<string name="screen_invites_empty_list">"მოწვევები არ არის"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) მოგიწვიათ"</string>
</resources>

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_invites_decline_chat_message">"Tens a certeza que queres rejeitar o convite para %1$s?"</string>
<string name="screen_invites_decline_chat_title">"Rejeitar conite"</string>
<string name="screen_invites_decline_direct_chat_message">"Tens a certeza que queres rejeitar esta conversa privada com %1$s?"</string>
<string name="screen_invites_decline_direct_chat_title">"Rejeitar conversa"</string>
<string name="screen_invites_empty_list">"Sem convites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) convidou-te"</string>
</resources>

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_invites_decline_chat_message">"您确定要拒绝加入 %1$s 的邀请吗?"</string>
<string name="screen_invites_decline_chat_title">"拒绝邀请"</string>
<string name="screen_invites_decline_direct_chat_message">"您确定要拒绝与 %1$s 开始私聊吗?"</string>
<string name="screen_invites_decline_direct_chat_title">"拒绝聊天"</string>
<string name="screen_invites_empty_list">"没有邀请"</string>
<string name="screen_invites_invited_you">"%1$s %2$s邀请了你"</string>
</resources>

View file

@ -17,6 +17,7 @@
package io.element.android.features.invite.impl.response
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
@ -26,13 +27,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -163,13 +164,10 @@ class AcceptDeclineInvitePresenterTest {
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomId: RoomId ->
val joinRoomFailure = lambdaRecorder { roomId: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
Result.failure<Unit>(RuntimeException("Failed to join room $roomId"))
}
val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomFailure
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -177,33 +175,37 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.DismissAcceptError
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomFailure).isCalledOnce()
assert(joinRoomFailure)
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
}
@Test
fun `present - accepting invite success flow`() = runTest {
val joinRoomSuccess = lambdaRecorder { _: RoomId ->
val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val client = FakeMatrixClient().apply {
joinRoomLambda = joinRoomSuccess
}
val presenter = createAcceptDeclineInvitePresenter(client = client)
val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomSuccess)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
@ -211,14 +213,22 @@ class AcceptDeclineInvitePresenterTest {
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.invite).isEqualTo(Optional.of(inviteData))
assertThat(state.invite).isEqualTo(Optional.empty<InviteData>())
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
assert(joinRoomSuccess).isCalledOnce()
assert(joinRoomSuccess)
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
}
private fun anInviteData(
@ -235,12 +245,14 @@ class AcceptDeclineInvitePresenterTest {
private fun createAcceptDeclineInvitePresenter(
client: MatrixClient = FakeMatrixClient(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(),
): AcceptDeclineInvitePresenter {
return AcceptDeclineInvitePresenter(
client = client,
analyticsService = analyticsService,
joinRoom = FakeJoinRoom(joinRoomLambda),
notificationDrawerManager = notificationDrawerManager,
)
}

View file

@ -26,4 +26,5 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.features.roomdirectory.api)
implementation(projects.services.analytics.api)
}

View file

@ -18,6 +18,7 @@ package io.element.android.features.joinroom.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
@ -32,5 +33,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>,
val serverNames: List<String>,
val trigger: JoinedRoom.Trigger,
) : NodeInputs
}

View file

@ -44,9 +44,10 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.features.invite.api)
implementation(projects.features.roomdirectory.api)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View file

@ -41,6 +41,8 @@ class JoinRoomNode @AssistedInject constructor(
inputs.roomId,
inputs.roomIdOrAlias,
inputs.roomDescription,
inputs.serverNames,
inputs.trigger,
)
@Composable
@ -48,14 +50,15 @@ class JoinRoomNode @AssistedInject constructor(
val state = presenter.present()
JoinRoomView(
state = state,
onBackPressed = ::navigateUp,
onBackClick = ::navigateUp,
onJoinSuccess = ::navigateUp,
onKnockSuccess = ::navigateUp,
modifier = modifier
)
acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState,
onInviteAccepted = {},
onInviteDeclined = { navigateUp() },
onAcceptInvite = {},
onDeclineInvite = { navigateUp() },
modifier = Modifier
)
}

View file

@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData
@ -41,10 +42,10 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.coroutines.CoroutineScope
@ -55,7 +56,10 @@ class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>,
@Assisted private val serverNames: List<String>,
@Assisted private val trigger: JoinedRoom.Trigger,
private val matrixClient: MatrixClient,
private val joinRoom: JoinRoom,
private val knockRoom: KnockRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val buildMeta: BuildMeta,
@ -65,6 +69,8 @@ class JoinRoomPresenter @AssistedInject constructor(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
serverNames: List<String>,
trigger: JoinedRoom.Trigger,
): JoinRoomPresenter
}
@ -73,6 +79,7 @@ class JoinRoomPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val contentState by produceState<ContentState>(
initialValue = ContentState.Loading(roomIdOrAlias),
@ -88,7 +95,7 @@ class JoinRoomPresenter @AssistedInject constructor(
}
else -> {
value = ContentState.Loading(roomIdOrAlias)
val result = matrixClient.getRoomPreview(roomId.toRoomIdOrAlias())
val result = matrixClient.getRoomPreviewFromRoomId(roomId, serverNames)
value = result.fold(
onSuccess = { roomPreview ->
roomPreview.toContentState()
@ -108,16 +115,14 @@ class JoinRoomPresenter @AssistedInject constructor(
fun handleEvents(event: JoinRoomEvents) {
when (event) {
JoinRoomEvents.AcceptInvite,
JoinRoomEvents.JoinRoom -> {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction)
JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
JoinRoomEvents.KnockRoom -> {
coroutineScope.knockRoom(roomId, knockAction)
}
JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink(
@ -129,6 +134,7 @@ class JoinRoomPresenter @AssistedInject constructor(
}
JoinRoomEvents.ClearError -> {
knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized
}
}
}
@ -136,13 +142,24 @@ class JoinRoomPresenter @AssistedInject constructor(
return JoinRoomState(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction.value,
knockAction = knockAction.value,
applicationName = buildMeta.applicationName,
eventSink = ::handleEvents
)
}
private fun CoroutineScope.knockRoom(roomId: RoomId, knockAction: MutableState<AsyncAction<Unit>>) = launch {
private fun CoroutineScope.joinRoom(joinAction: MutableState<AsyncAction<Unit>>) = launch {
joinAction.runUpdatingState {
joinRoom.invoke(
roomId = roomId,
serverNames = serverNames,
trigger = trigger
)
}
}
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>) = launch {
knockAction.runUpdatingState {
knockRoom(roomId)
}

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.ui.model.InviteSender
data class JoinRoomState(
val contentState: ContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val joinAction: AsyncAction<Unit>,
val knockAction: AsyncAction<Unit>,
val applicationName: String,
val eventSink: (JoinRoomEvents) -> Unit

View file

@ -125,11 +125,13 @@ fun aLoadedContentState(
fun aJoinRoomState(
contentState: ContentState = aLoadedContentState(),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState(
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction,
knockAction = knockAction,
applicationName = "AppName",
eventSink = eventSink

View file

@ -65,7 +65,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun JoinRoomView(
state: JoinRoomState,
onBackPressed: () -> Unit,
onBackClick: () -> Unit,
onJoinSuccess: () -> Unit,
onKnockSuccess: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -77,7 +78,7 @@ fun JoinRoomView(
containerColor = Color.Transparent,
paddingValues = PaddingValues(16.dp),
topBar = {
JoinRoomTopBar(onBackClicked = onBackPressed)
JoinRoomTopBar(onBackClick = onBackClick)
},
content = {
JoinRoomContent(
@ -103,12 +104,16 @@ fun JoinRoomView(
onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent)
},
onGoBack = onBackPressed,
onGoBack = onBackClick,
)
}
)
}
AsyncActionView(
async = state.joinAction,
onSuccess = { onJoinSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) },
)
AsyncActionView(
async = state.knockAction,
onSuccess = { onKnockSuccess() },
@ -307,11 +312,11 @@ private fun JoinRoomContent(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun JoinRoomTopBar(
onBackClicked: () -> Unit,
onBackClick: () -> Unit,
) {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClicked)
BackButton(onClick = onBackClick)
},
title = {},
)
@ -322,7 +327,8 @@ private fun JoinRoomTopBar(
internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
JoinRoomView(
state = state,
onBackPressed = { },
onBackClick = { },
onJoinSuccess = { },
onKnockSuccess = { },
)
}

View file

@ -19,6 +19,7 @@ package io.element.android.features.joinroom.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.joinroom.impl.JoinRoomPresenter
import io.element.android.features.roomdirectory.api.RoomDescription
@ -28,6 +29,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.join.JoinRoom
import java.util.Optional
@Module
@ -36,6 +38,7 @@ object JoinRoomModule {
@Provides
fun providesJoinRoomPresenterFactory(
client: MatrixClient,
joinRoom: JoinRoom,
knockRoom: KnockRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
buildMeta: BuildMeta,
@ -45,12 +48,17 @@ object JoinRoomModule {
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
serverNames: List<String>,
trigger: JoinedRoom.Trigger,
): JoinRoomPresenter {
return JoinRoomPresenter(
roomId = roomId,
roomIdOrAlias = roomIdOrAlias,
roomDescription = roomDescription,
serverNames = serverNames,
trigger = trigger,
matrixClient = client,
joinRoom = joinRoom,
knockRoom = knockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
buildMeta = buildMeta,

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="screen_join_room_join_action">"Entra nella stanza"</string>
<string name="screen_join_room_knock_action">"Bussa per partecipare"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s non supporta ancora gli spazi. Puoi accedere agli spazi sul web."</string>
<string name="screen_join_room_space_not_supported_title">"Gli spazi non sono ancora supportati"</string>
<string name="screen_join_room_subtitle_knock">"Clicca sul pulsante qui sotto e un amministratore della stanza riceverà una notifica. Potrai partecipare alla conversazione una volta approvato."</string>
<string name="screen_join_room_subtitle_no_preview">"Per visualizzare la cronologia dei messaggi devi essere un membro di questa stanza."</string>
<string name="screen_join_room_title_knock">"Vuoi entrare in questa stanza?"</string>
<string name="screen_join_room_title_no_preview">"L\'anteprima non è disponibile"</string>
</resources>

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="screen_join_room_join_action">"Entrar na sala"</string>
<string name="screen_join_room_knock_action">"Bater à porta"</string>
<string name="screen_join_room_space_not_supported_description">"A %1$s ainda não funciona com espaços. Podes usá-los na aplicação web."</string>
<string name="screen_join_room_space_not_supported_title">"Os espaços ainda não estão implementados"</string>
<string name="screen_join_room_subtitle_knock">"Carrega no botão abaixo para notificar um administrador da sala. Poderás entrar quando te aprovarem."</string>
<string name="screen_join_room_subtitle_no_preview">"Apenas os participantes podem ver o histórico de mensagens."</string>
<string name="screen_join_room_title_knock">"Queres entrar nesta sala?"</string>
<string name="screen_join_room_title_no_preview">"Pré-visualização indisponível"</string>
</resources>

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="screen_join_room_join_action">"Alăturați-vă camerei"</string>
<string name="screen_join_room_knock_action">"Bateți pentru a vă alătura"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s nu suporta încă spații. Puteți accesa spațiile pe web."</string>
<string name="screen_join_room_space_not_supported_title">"Spațiile nu sunt încă suportate"</string>
<string name="screen_join_room_subtitle_knock">"Faceți clic pe butonul de mai jos și un administrator de cameră va fi notificat. Veți putea să vă alăturați conversației odată aprobată."</string>
<string name="screen_join_room_subtitle_no_preview">"Trebuie să fiți membru al acestei camere pentru a vizualiza istoricul mesajelor."</string>
<string name="screen_join_room_title_knock">"Doriți să vă alăturați acestei camere?"</string>
<string name="screen_join_room_title_no_preview">"Previzualizare indisponibilă"</string>
</resources>

View file

@ -2,6 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_join_action">"Присоединиться к комнате"</string>
<string name="screen_join_room_knock_action">"Постучите, чтобы присоединиться"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s еще не поддерживает пространства. Вы можете получить к ним доступ в веб-версии."</string>
<string name="screen_join_room_space_not_supported_title">"Пространства пока не поддерживаются"</string>
<string name="screen_join_room_subtitle_knock">"Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению."</string>
<string name="screen_join_room_subtitle_no_preview">"Вы должны быть участником этой комнаты, чтобы просмотреть историю сообщений."</string>
<string name="screen_join_room_title_knock">"Хотите присоединиться к этой комнате?"</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="screen_join_room_join_action">"加入聊天室"</string>
<string name="screen_join_room_knock_action">"加入房间"</string>
<string name="screen_join_room_space_not_supported_description">"%1$s 尚不支持空间。您可以通过 Web 端访问空间"</string>
<string name="screen_join_room_space_not_supported_title">"空间尚不支持"</string>
<string name="screen_join_room_subtitle_knock">"点击下面的按钮,系统将通知房间管理员。获得批准后,您将能够加入对话。"</string>
<string name="screen_join_room_subtitle_no_preview">"只有聊天室成员才能查看消息历史记录。"</string>
<string name="screen_join_room_title_knock">"想加入这个房间吗?"</string>
<string name="screen_join_room_title_no_preview">"预览不可用"</string>
</resources>

View file

@ -17,6 +17,7 @@
package io.element.android.features.joinroom.impl
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
@ -36,10 +37,12 @@ import io.element.android.libraries.matrix.api.room.preview.RoomPreview
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
@ -174,6 +177,59 @@ class JoinRoomPresenterTest {
}
}
@Test
fun `present - when room is joined with success, all the parameters are provided`() = runTest {
val aTrigger = JoinedRoom.Trigger.MobilePermalink
val joinRoomLambda = lambdaRecorder { _: RoomId, _: List<String>, _: JoinedRoom.Trigger ->
Result.success(Unit)
}
val presenter = createJoinRoomPresenter(
trigger = aTrigger,
serverNames = A_SERVER_LIST,
joinRoomLambda = joinRoomLambda,
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Success(Unit))
}
joinRoomLambda.assertions()
.isCalledOnce()
.with(value(A_ROOM_ID), value(A_SERVER_LIST), value(aTrigger))
}
}
@Test
fun `present - when room is joined with error, it is possible to clear the error`() = runTest {
val presenter = createJoinRoomPresenter(
joinRoomLambda = { _, _, _ ->
Result.failure(AN_EXCEPTION)
},
)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
state.eventSink(JoinRoomEvents.ClearError)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
}
}
}
@Test
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true)
@ -310,7 +366,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
getRoomPreviewFromRoomIdResult = { _, _ ->
Result.success(
RoomPreview(
roomId = A_ROOM_ID,
@ -355,7 +411,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
getRoomPreviewFromRoomIdResult = { _, _ ->
Result.failure(AN_EXCEPTION)
}
)
@ -393,7 +449,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is not known RoomPreview is loaded with error 403`() = runTest {
val client = FakeMatrixClient(
getRoomPreviewResult = {
getRoomPreviewFromRoomIdResult = { _, _ ->
Result.failure(Exception("403"))
}
)
@ -415,7 +471,12 @@ class JoinRoomPresenterTest {
private fun createJoinRoomPresenter(
roomId: RoomId = A_ROOM_ID,
roomDescription: Optional<RoomDescription> = Optional.empty(),
serverNames: List<String> = emptyList(),
trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite,
matrixClient: MatrixClient = FakeMatrixClient(),
joinRoomLambda: (RoomId, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
Result.success(Unit)
},
knockRoom: KnockRoom = FakeKnockRoom(),
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
@ -424,7 +485,10 @@ class JoinRoomPresenterTest {
roomId = roomId,
roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription,
serverNames = serverNames,
trigger = trigger,
matrixClient = matrixClient,
joinRoom = FakeJoinRoom(joinRoomLambda),
knockRoom = knockRoom,
buildMeta = buildMeta,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter

View file

@ -45,7 +45,7 @@ class JoinRoomViewTest {
aJoinRoomState(
eventSink = eventsRecorder,
),
onBackPressed = it
onBackClick = it
)
rule.pressBack()
}
@ -91,6 +91,34 @@ class JoinRoomViewTest {
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
}
@Test
fun `clicking on closing Join error emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
joinAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearError)
}
@Test
fun `when joining room is successful, the expected callback is invoked`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
ensureCalledOnce {
rule.setJoinRoomView(
aJoinRoomState(
joinAction = AsyncAction.Success(Unit),
eventSink = eventsRecorder,
),
onJoinSuccess = it
)
}
}
@Test
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
@ -139,7 +167,7 @@ class JoinRoomViewTest {
contentState = aLoadedContentState(roomType = RoomType.Space),
eventSink = eventsRecorder,
),
onBackPressed = it
onBackClick = it
)
rule.clickOn(CommonStrings.action_go_back)
}
@ -148,13 +176,15 @@ class JoinRoomViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinRoomView(
state: JoinRoomState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(),
onJoinSuccess: () -> Unit = EnsureNeverCalled(),
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
) {
setContent {
JoinRoomView(
state = state,
onBackPressed = onBackPressed,
onBackClick = onBackClick,
onJoinSuccess = onJoinSuccess,
onKnockSuccess = onKnockSuccess,
)
}

View file

@ -89,7 +89,7 @@ private fun LeaveRoomConfirmationDialog(
title = stringResource(if (isDm) CommonStrings.action_leave_conversation else CommonStrings.action_leave_room),
content = stringResource(text),
submitText = stringResource(CommonStrings.action_leave),
onSubmitClicked = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
onSubmitClick = { eventSink(LeaveRoomEvent.LeaveRoom(roomId)) },
onDismiss = { eventSink(LeaveRoomEvent.HideConfirmation) },
)
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"¿Estás seguro de que quieres salir de esta conversación? Esta conversación no es pública y no podrás volver a unirte sin una invitación."</string>
<string name="leave_room_alert_empty_subtitle">"¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú."</string>
<string name="leave_room_alert_private_subtitle">"¿Estás seguro de que quieres abandonar esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación."</string>
<string name="leave_room_alert_subtitle">"¿Seguro que quieres salir de la habitación?"</string>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_room_alert_empty_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? თქვენ აქ მარტო ხართ და ჩატის დატოვებისას აქ თქვენს ჩათვლით ვერავინ ვერ გაწევრიანდება."</string>
<string name="leave_room_alert_private_subtitle">"დარწმუნებული ბრძანდებით, რომ ამ ოთახის დატოვება გსურთ? ეს ოთახი არ არის საჯარო და მოწვევის გარეშე ვერ შეძლებთ ხელახლა გაწევრიანებას."</string>
<string name="leave_room_alert_subtitle">"დარწმუნებული ბრძანდებით, რომ ოთახის დატოვება გსურთ?"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"Tens a certeza que queres sair desta conversa? Não é pública, logo não poderás voltar a participar sem um convite."</string>
<string name="leave_room_alert_empty_subtitle">"Tens a certeza que queres sair desta sala? És o único participante. Se saíres, ninguém mais poderá entrar, incluindo tu."</string>
<string name="leave_room_alert_private_subtitle">"Tens a certeza que queres sair desta sala? Atenta que não é pública e portanto não poderás voltar a entrar sem um novo convite."</string>
<string name="leave_room_alert_subtitle">"Tens a certeza que queres sair da sala?"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="leave_conversation_alert_subtitle">"您确定要离开此对话吗?此对话不公开,未经邀请您将无法重新加入。"</string>
<string name="leave_room_alert_empty_subtitle">"你确定要离开这个房间吗?这里只有你一个人,如果你走了,包括你在内的所有人都无法进入此房间。"</string>
<string name="leave_room_alert_private_subtitle">"你确定要离开这个房间吗?这个房间不是公开的,如果没有邀请,你将无法重新加入。"</string>
<string name="leave_room_alert_subtitle">"你确定要离开房间吗?"</string>
</resources>

View file

@ -29,7 +29,7 @@ internal fun PermissionDeniedDialog(
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClicked = onContinue,
onSubmitClick = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),

View file

@ -29,7 +29,7 @@ internal fun PermissionRationaleDialog(
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClicked = onContinue,
onSubmitClick = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),

View file

@ -54,7 +54,7 @@ class ShowLocationNode @AssistedInject constructor(
ShowLocationView(
state = presenter.present(),
modifier = modifier,
onBackPressed = ::navigateUp
onBackClick = ::navigateUp
)
}
}

View file

@ -67,7 +67,7 @@ import org.maplibre.android.geometry.LatLng
@Composable
fun ShowLocationView(
state: ShowLocationState,
onBackPressed: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (state.permissionDialog) {
@ -121,7 +121,7 @@ fun ShowLocationView(
},
navigationIcon = {
BackButton(
onClick = onBackPressed,
onClick = onBackClick,
)
},
actions = {
@ -194,7 +194,7 @@ fun ShowLocationView(
internal fun ShowLocationViewPreview(@PreviewParameter(ShowLocationStateProvider::class) state: ShowLocationState) = ElementPreview {
ShowLocationView(
state = state,
onBackPressed = {},
onBackClick = {},
)
}

View file

@ -18,7 +18,7 @@ package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable
class PermissionsPresenterFake : PermissionsPresenter {
class FakePermissionsPresenter : PermissionsPresenter {
val events = mutableListOf<PermissionsEvents>()
private fun handleEvent(event: PermissionsEvents) {

View file

@ -24,9 +24,9 @@ import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.room.location.AssetType
@ -45,7 +45,7 @@ class SendLocationPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake()
private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeMatrixRoom = FakeMatrixRoom()
private val fakeAnalyticsService = FakeAnalyticsService()
private val fakeMessageComposerContext = FakeMessageComposerContext()
@ -53,7 +53,7 @@ class SendLocationPresenterTest {
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
},
room = fakeMatrixRoom,
analyticsService = fakeAnalyticsService,
@ -64,7 +64,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions granted`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
@ -90,7 +90,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions partially granted`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.SomeGranted,
shouldShowRationale = false,
@ -116,7 +116,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions denied`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -142,7 +142,7 @@ class SendLocationPresenterTest {
@Test
fun `initial state with permissions denied once`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -168,7 +168,7 @@ class SendLocationPresenterTest {
@Test
fun `rationale dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -199,7 +199,7 @@ class SendLocationPresenterTest {
@Test
fun `rationale dialog continue`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -221,13 +221,13 @@ class SendLocationPresenterTest {
// Continue the dialog sends permission request to the permissions presenter
myLocationState.eventSink(SendLocationEvents.RequestPermissions)
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
}
}
@Test
fun `permission denied dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -258,7 +258,7 @@ class SendLocationPresenterTest {
@Test
fun `share sender location`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
shouldShowRationale = false,
@ -314,7 +314,7 @@ class SendLocationPresenterTest {
@Test
fun `share pin location`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -370,7 +370,7 @@ class SendLocationPresenterTest {
@Test
fun `composer context passes through analytics`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -418,7 +418,7 @@ class SendLocationPresenterTest {
@Test
fun `open settings activity`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,

View file

@ -23,9 +23,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
@ -38,13 +38,13 @@ class ShowLocationPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
private val permissionsPresenterFake = PermissionsPresenterFake()
private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val location = Location(1.23, 4.56, 7.8f)
private val presenter = ShowLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
},
fakeLocationActions,
fakeBuildMeta,
@ -54,7 +54,7 @@ class ShowLocationPresenterTest {
@Test
fun `emits initial state with no location permission`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -74,7 +74,7 @@ class ShowLocationPresenterTest {
@Test
fun `emits initial state location permission denied once`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -94,7 +94,7 @@ class ShowLocationPresenterTest {
@Test
fun `emits initial state with location permission`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -109,7 +109,7 @@ class ShowLocationPresenterTest {
@Test
fun `emits initial state with partial location permission`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -137,7 +137,7 @@ class ShowLocationPresenterTest {
@Test
fun `centers on user location`() = runTest {
permissionsPresenterFake.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -165,7 +165,7 @@ class ShowLocationPresenterTest {
@Test
fun `rationale dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -196,7 +196,7 @@ class ShowLocationPresenterTest {
@Test
fun `rationale dialog continue`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
@ -218,13 +218,13 @@ class ShowLocationPresenterTest {
// Continue the dialog sends permission request to the permissions presenter
trackLocationState.eventSink(ShowLocationEvents.RequestPermissions)
assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
}
}
@Test
fun `permission denied dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
@ -255,7 +255,7 @@ class ShowLocationPresenterTest {
@Test
fun `open settings activity`() = runTest {
permissionsPresenterFake.givenState(
fakePermissionsPresenter.givenState(
aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,

View file

@ -49,7 +49,7 @@ class ShowLocationViewTest {
state = aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = callback,
onBackClick = callback,
)
rule.pressBack()
}
@ -62,7 +62,7 @@ class ShowLocationViewTest {
aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
rule.onNodeWithContentDescription(shareContentDescription).performClick()
@ -76,7 +76,7 @@ class ShowLocationViewTest {
aShowLocationState(
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick()
eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true))
@ -90,7 +90,7 @@ class ShowLocationViewTest {
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvents.OpenAppSettings)
@ -104,7 +104,7 @@ class ShowLocationViewTest {
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
@ -118,7 +118,7 @@ class ShowLocationViewTest {
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(ShowLocationEvents.RequestPermissions)
@ -132,7 +132,7 @@ class ShowLocationViewTest {
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
eventSink = eventsRecorder
),
onBackPressed = EnsureNeverCalled(),
onBackClick = EnsureNeverCalled(),
)
rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog)
@ -141,14 +141,14 @@ class ShowLocationViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView(
state: ShowLocationState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
// Simulate a LocalInspectionMode for MapLibreMap
CompositionLocalProvider(LocalInspectionMode provides true) {
ShowLocationView(
state = state,
onBackPressed = onBackPressed,
onBackClick = onBackClick,
)
}
}

View file

@ -16,17 +16,19 @@
package io.element.android.features.lockscreen.api
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
interface LockScreenEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder
fun pinUnlockIntent(context: Context): Intent
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun target(target: Target): NodeBuilder
fun build(): Node
}
@ -37,6 +39,5 @@ interface LockScreenEntryPoint : FeatureEntryPoint {
enum class Target {
Settings,
Setup,
Unlock
}
}

View file

@ -0,0 +1,25 @@
<!--
~ Copyright (c) 2024 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
</application>
</manifest>

View file

@ -16,18 +16,20 @@
package io.element.android.features.lockscreen.impl
import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder {
var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>()
return object : LockScreenEntryPoint.NodeBuilder {
@ -36,15 +38,9 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
return this
}
override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
innerTarget = target
return this
}
override fun build(): Node {
val inputs = LockScreenFlowNode.Inputs(
when (innerTarget) {
LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock
when (navTarget) {
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
}
@ -54,4 +50,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
}
}
}
override fun pinUnlockIntent(context: Context): Intent {
return PinUnlockActivity.newIntent(context)
}
}

View file

@ -30,7 +30,6 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
@ -44,20 +43,17 @@ class LockScreenFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget,
initialElement = plugins.filterIsInstance<Inputs>().first().initialNavTarget,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
data class Inputs(
val initialNavTarget: NavTarget = NavTarget.Unlock,
val initialNavTarget: NavTarget,
) : NodeInputs
sealed interface NavTarget : Parcelable {
@Parcelize
data object Unlock : NavTarget
@Parcelize
data object Setup : NavTarget
@ -75,10 +71,6 @@ class LockScreenFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Unlock -> {
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
}
NavTarget.Setup -> {
val callback = OnSetupDoneCallback(plugins())
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))

View file

@ -103,20 +103,19 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Unlock -> {
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
val callback = object : PinUnlockNode.Callback {
override fun onUnlock() {
backstack.newRoot(NavTarget.Settings)
}
}
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs, callback))
createNode<PinUnlockNode>(buildContext, plugins = listOf(callback))
}
NavTarget.SetupPin -> {
createNode<SetupPinNode>(buildContext)
}
NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback {
override fun onChangePinClicked() {
override fun onChangePinClick() {
backstack.push(NavTarget.SetupPin)
}
}

View file

@ -34,11 +34,11 @@ class LockScreenSettingsNode @AssistedInject constructor(
private val presenter: LockScreenSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onChangePinClicked()
fun onChangePinClick()
}
private fun onChangePinClicked() {
plugins<Callback>().forEach { it.onChangePinClicked() }
private fun onChangePinClick() {
plugins<Callback>().forEach { it.onChangePinClick() }
}
@Composable
@ -46,8 +46,8 @@ class LockScreenSettingsNode @AssistedInject constructor(
val state = presenter.present()
LockScreenSettingsView(
state = state,
onBackPressed = this::navigateUp,
onChangePinClicked = this::onChangePinClicked,
onBackClick = this::navigateUp,
onChangePinClick = this::onChangePinClick,
modifier = modifier,
)
}

View file

@ -34,19 +34,19 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@Composable
fun LockScreenSettingsView(
state: LockScreenSettingsState,
onChangePinClicked: () -> Unit,
onBackPressed: () -> Unit,
onChangePinClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferencePage(
title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock),
onBackPressed = onBackPressed,
onBackClick = onBackClick,
modifier = modifier
) {
PreferenceCategory(showDivider = false) {
PreferenceCategory(showTopDivider = false) {
PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
onClick = onChangePinClicked
onClick = onChangePinClick
)
PreferenceDivider()
if (state.showRemovePinOption) {
@ -74,7 +74,7 @@ fun LockScreenSettingsView(
ConfirmationDialog(
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_title),
content = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_message),
onSubmitClicked = {
onSubmitClick = {
state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin)
},
onDismiss = {
@ -92,8 +92,8 @@ internal fun LockScreenSettingsViewPreview(
ElementPreview {
LockScreenSettingsView(
state = state,
onChangePinClicked = {},
onBackPressed = {},
onChangePinClick = {},
onBackClick = {},
)
}
}

View file

@ -49,8 +49,8 @@ fun SetupBiometricView(
},
footer = {
SetupBiometricFooter(
onAllowClicked = { state.eventSink(SetupBiometricEvents.AllowBiometric) },
onSkipClicked = { state.eventSink(SetupBiometricEvents.UsePin) }
onAllowClick = { state.eventSink(SetupBiometricEvents.AllowBiometric) },
onSkipClick = { state.eventSink(SetupBiometricEvents.UsePin) }
)
},
)
@ -68,18 +68,18 @@ private fun SetupBiometricHeader() {
@Composable
private fun SetupBiometricFooter(
onAllowClicked: () -> Unit,
onSkipClicked: () -> Unit,
onAllowClick: () -> Unit,
onSkipClick: () -> Unit,
) {
ButtonColumnMolecule {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
Button(
text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_allow_title, biometricAuth),
onClick = onAllowClicked
onClick = onAllowClick
)
TextButton(
text = stringResource(id = R.string.screen_app_lock_setup_biometric_unlock_skip),
onClick = onSkipClicked
onClick = onSkipClick
)
}
}

View file

@ -37,7 +37,7 @@ class SetupPinNode @AssistedInject constructor(
val state = presenter.present()
SetupPinView(
state = state,
onBackClicked = this::navigateUp,
onBackClick = this::navigateUp,
modifier = modifier
)
}

View file

@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
@Composable
fun SetupPinView(
state: SetupPinState,
onBackClicked: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -60,7 +60,7 @@ fun SetupPinView(
topBar = {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClicked)
BackButton(onClick = onBackClick)
},
title = {}
)
@ -154,7 +154,7 @@ internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class)
ElementPreview {
SetupPinView(
state = state,
onBackClicked = {},
onBackClick = {},
)
}
}

View file

@ -26,8 +26,6 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@ -40,12 +38,6 @@ class PinUnlockNode @AssistedInject constructor(
fun onUnlock()
}
data class Inputs(
val isInAppUnlock: Boolean
) : NodeInputs
private val inputs: Inputs = inputs()
private fun onUnlock() {
plugins<Callback>().forEach {
it.onUnlock()
@ -62,7 +54,9 @@ class PinUnlockNode @AssistedInject constructor(
}
PinUnlockView(
state = state,
isInAppUnlock = inputs.isInAppUnlock,
// UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
// It's set to false in PinUnlockActivity.
isInAppUnlock = true,
modifier = modifier
)
}

View file

@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -41,7 +41,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager,
private val matrixClient: MatrixClient,
private val signOut: SignOut,
private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper,
) : Presenter<PinUnlockState> {
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend {
matrixClient.logout(ignoreSdkError = true)
signOut()
}.runCatchingUpdatingState(signOutAction)
}
}

View file

@ -192,7 +192,7 @@ private fun SignOutPrompt(
ConfirmationDialog(
title = stringResource(id = R.string.screen_app_lock_signout_alert_title),
content = stringResource(id = R.string.screen_app_lock_signout_alert_message),
onSubmitClicked = onSignOut,
onSubmitClick = onSignOut,
onDismiss = onDismiss,
)
} else {

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
import io.element.android.libraries.architecture.bindings
import kotlinx.coroutines.launch
import javax.inject.Inject
class PinUnlockActivity : AppCompatActivity() {
internal companion object {
fun newIntent(context: Context): Intent {
return Intent(context, PinUnlockActivity::class.java)
}
}
@Inject lateinit var presenter: PinUnlockPresenter
@Inject lateinit var lockScreenService: LockScreenService
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this)
setContent {
ElementTheme {
val state = presenter.present()
PinUnlockView(state = state, isInAppUnlock = false)
}
}
lifecycleScope.launch {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Unlocked) {
finish()
}
}
}
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
moveTaskToBack(true)
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
}

View file

@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.features.roomdirectory.impl.root
package io.element.android.features.lockscreen.impl.unlock.di
import io.element.android.features.roomdirectory.impl.root.di.JoinRoom
import io.element.android.libraries.matrix.api.core.RoomId
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.di.AppScope
class FakeJoinRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : JoinRoom {
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
@ContributesTo(AppScope::class)
interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity)
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.unlock.signout
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSignOut @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val matrixClientProvider: MatrixClientProvider,
) : SignOut {
override suspend fun invoke(): String? {
val currentSession = authenticationService.getLatestSessionId()
return if (currentSession != null) {
matrixClientProvider.getOrRestore(currentSession)
.getOrThrow()
.logout(ignoreSdkError = true)
} else {
error("No session to sign out")
}
}
}

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