Merge branch 'main' into wallet

# Conflicts:
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt
#	features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt
#	libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt
This commit is contained in:
Cobb 2026-04-16 22:05:16 -07:00
commit 0ef6b69a79
912 changed files with 17051 additions and 4425 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

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_help_us_improve">"Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas."</string>
<string name="screen_analytics_settings_help_us_improve">"Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas."</string>
<string name="screen_analytics_settings_read_terms">"Galite perskaityti visas mūsų sąlygas %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"čia"</string>
<string name="screen_analytics_settings_share_data">"Dalytis analitiniais duomenimis"</string>
<string name="screen_analytics_settings_share_data">"Bendrinti analitinius duomenis"</string>
</resources>

View file

@ -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">"Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề."</string>
<string name="screen_analytics_settings_read_terms">"Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s"</string>
<string name="screen_analytics_settings_read_terms_content_link">"tại đây"</string>
<string name="screen_analytics_settings_share_data">"Chia sẻ dữ liệu phân tích"</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

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

View file

@ -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">"Chúng tôi sẽ không ghi lại hoặc lập hồ sơ bất kỳ dữ liệu cá nhân nào."</string>
<string name="screen_analytics_prompt_help_us_improve">"Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề."</string>
<string name="screen_analytics_prompt_read_terms">"Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"tại đây"</string>
<string name="screen_analytics_prompt_settings">"Bạn có thể tắt tính năng này bất cứ lúc nào"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Chúng tôi sẽ không chia sẻ dữ liệu của bạn với bên thứ ba."</string>
<string name="screen_analytics_prompt_title">"Giúp cải thiện %1$s"</string>
</resources>

View file

@ -8,7 +8,13 @@
package io.element.android.features.announcement.api
enum class Announcement {
Space,
NewNotificationSound,
import androidx.compose.runtime.Immutable
@Immutable
sealed interface Announcement {
enum class Fullscreen : Announcement {
Space,
}
data object NewNotificationSound : Announcement
}

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl
import io.element.android.features.announcement.api.Announcement
sealed interface AnnouncementEvent {
data class Continue(
val announcement: Announcement.Fullscreen,
) : AnnouncementEvent
}

View file

@ -12,12 +12,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Inject
class AnnouncementPresenter(
@ -25,13 +29,39 @@ class AnnouncementPresenter(
) : Presenter<AnnouncementState> {
@Composable
override fun present(): AnnouncementState {
val showSpaceAnnouncement by remember {
announcementStore.announcementStatusFlow(Announcement.Space).map {
it == AnnouncementStatus.Show
val coroutineScope = rememberCoroutineScope()
val fullscreenAnnouncementToShow by remember {
combine(
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).map {
it == AnnouncementStatus.Show
},
// Add other announcements here when needed
) { _, showFullscreenSpace ->
when {
showFullscreenSpace -> Announcement.Fullscreen.Space
else -> {
null
}
}
}
}.collectAsState(false)
}.collectAsState(null)
fun handle(event: AnnouncementEvent) {
when (event) {
is AnnouncementEvent.Continue -> coroutineScope.launch {
announcementStore.setAnnouncementStatus(
announcement = event.announcement,
status = AnnouncementStatus.Shown,
)
}
}
}
return AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
announcement = fullscreenAnnouncementToShow,
eventSink = ::handle,
)
}
}

View file

@ -8,12 +8,9 @@
package io.element.android.features.announcement.impl
data class AnnouncementState(
val showSpaceAnnouncement: Boolean,
)
import io.element.android.features.announcement.api.Announcement
fun anAnnouncementState(
showSpaceAnnouncement: Boolean = false,
) = AnnouncementState(
showSpaceAnnouncement = showSpaceAnnouncement,
data class AnnouncementState(
val announcement: Announcement.Fullscreen?,
val eventSink: (AnnouncementEvent) -> Unit,
)

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.announcement.api.Announcement
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> {
override val values: Sequence<AnnouncementState>
get() = sequenceOf(
anAnnouncementState(),
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
),
)
}
fun anAnnouncementState(
announcement: Announcement.Fullscreen? = null,
eventSink: (AnnouncementEvent) -> Unit = {},
) = AnnouncementState(
announcement = announcement,
eventSink = eventSink,
)

View file

@ -8,35 +8,28 @@
package io.element.android.features.announcement.impl
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView
import io.element.android.features.announcement.impl.fullscreen.FullscreenAnnouncementView
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultAnnouncementService(
private val announcementStore: AnnouncementStore,
private val announcementPresenter: Presenter<AnnouncementState>,
private val spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState>,
private val announcementPresenter: AnnouncementPresenter,
) : AnnouncementService {
override suspend fun showAnnouncement(announcement: Announcement) {
when (announcement) {
Announcement.Space -> showSpaceAnnouncement()
is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement)
Announcement.NewNotificationSound -> {
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
}
@ -49,13 +42,10 @@ class DefaultAnnouncementService(
override fun announcementsToShowFlow(): Flow<List<Announcement>> {
return combine(
announcementStore.announcementStatusFlow(Announcement.Space),
flowOf(Unit),
announcementStore.announcementStatusFlow(Announcement.NewNotificationSound),
) { spaceAnnouncementStatus, newNotificationSoundStatus ->
) { _, newNotificationSoundStatus ->
buildList {
if (spaceAnnouncementStatus == AnnouncementStatus.Show) {
add(Announcement.Space)
}
if (newNotificationSoundStatus == AnnouncementStatus.Show) {
add(Announcement.NewNotificationSound)
}
@ -63,27 +53,19 @@ class DefaultAnnouncementService(
}
}
private suspend fun showSpaceAnnouncement() {
val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first()
private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) {
val currentValue = announcementStore.announcementStatusFlow(announcement).first()
if (currentValue == AnnouncementStatus.NeverShown) {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show)
}
}
@Composable
override fun Render(modifier: Modifier) {
val announcementState = announcementPresenter.present()
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = announcementState.showSpaceAnnouncement,
enter = fadeIn(),
exit = fadeOut(),
) {
val spaceAnnouncementState = spaceAnnouncementPresenter.present()
SpaceAnnouncementView(
state = spaceAnnouncementState,
)
}
}
FullscreenAnnouncementView(
state = announcementState,
modifier = modifier,
)
}
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.announcement.impl.AnnouncementPresenter
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.libraries.architecture.Presenter
@ContributesTo(AppScope::class)
@BindingContainer
interface AnnouncementModule {
@Binds
fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter<AnnouncementState>
@Binds
fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter<SpaceAnnouncementState>
}

View file

@ -0,0 +1,225 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.fullscreen
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.AnnouncementStateProvider
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
*/
@Composable
fun FullscreenAnnouncementView(
state: AnnouncementState,
modifier: Modifier = Modifier,
) {
// Ensure that the content stays visible during the exit animation
var fullscreenAnnouncement by remember { mutableStateOf<Announcement.Fullscreen?>(null) }
if (state.announcement != null) {
fullscreenAnnouncement = state.announcement
}
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = state.announcement != null,
enter = fadeIn(),
exit = fadeOut(),
) {
fullscreenAnnouncement?.let {
FullscreenAnnouncementView(
announcement = it,
eventSink = state.eventSink,
)
}
}
}
}
@Composable
private fun FullscreenAnnouncementView(
announcement: Announcement.Fullscreen,
eventSink: (AnnouncementEvent) -> Unit,
modifier: Modifier = Modifier
) {
fun onContinue() {
eventSink(AnnouncementEvent.Continue(announcement))
}
BackHandler(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
FullscreenAnnouncementHeader(announcement)
},
content = {
FullscreenAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
announcement = announcement,
)
},
footer = {
FullscreenAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun FullscreenAnnouncementHeader(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
title = announcement.title(),
showBetaLabel = true,
subTitle = announcement.subtitle(),
iconStyle = BigIcon.Style.Default(
vectorIcon = announcement.icon(),
usePrimaryTint = true,
),
)
}
@Composable
private fun FullscreenAnnouncementContent(
announcement: Announcement.Fullscreen,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = announcement.items(),
textStyle = ElementTheme.typography.fontBodyLgMedium,
iconTint = ElementTheme.colors.iconSecondary,
iconSize = 24.dp
)
announcement.notice()?.let { notice ->
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
text = notice,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
}
@Composable
private fun FullscreenAnnouncementFooter(
onContinue: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 8.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun Announcement.Fullscreen.title() = when (this) {
Announcement.Fullscreen.Space -> "Introducing Spaces"
}
@Composable
private fun Announcement.Fullscreen.subtitle() = when (this) {
Announcement.Fullscreen.Space -> "Welcome to the beta version of Spaces! With this first version you can:"
}
@Composable
private fun Announcement.Fullscreen.icon() = when (this) {
Announcement.Fullscreen.Space -> CompoundIcons.SpaceSolid()
}
@Composable
private fun Announcement.Fullscreen.items(): ImmutableList<InfoListItem> = when (this) {
Announcement.Fullscreen.Space -> persistentListOf(
InfoListItem(
message = "View spaces you\'ve created or joined",
iconVector = CompoundIcons.VisibilityOn(),
),
InfoListItem(
message = "Accept or decline invites to spaces",
iconVector = CompoundIcons.Email(),
),
InfoListItem(
message = "Discover any rooms you can join in your spaces",
iconVector = CompoundIcons.Search(),
),
InfoListItem(
message = "Join public spaces",
iconVector = CompoundIcons.Explore(),
),
InfoListItem(
message = "Leave any spaces youve joined",
iconVector = CompoundIcons.Leave(),
),
)
}
@Composable
private fun Announcement.Fullscreen.notice(): String? = when (this) {
Announcement.Fullscreen.Space -> "Filtering, creating and managing spaces is coming soon."
}
@PreviewsDayNight
@Composable
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
sealed interface SpaceAnnouncementEvents {
data object Continue : SpaceAnnouncementEvents
}

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
@Inject
class SpaceAnnouncementPresenter(
private val announcementStore: AnnouncementStore,
) : Presenter<SpaceAnnouncementState> {
@Composable
override fun present(): SpaceAnnouncementState {
val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: SpaceAnnouncementEvents) {
when (event) {
SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch {
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
}
}
}
return SpaceAnnouncementState(
eventSink = ::handleEvent,
)
}
}

View file

@ -1,13 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
data class SpaceAnnouncementState(
val eventSink: (SpaceAnnouncementEvents) -> Unit
)

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class SpaceAnnouncementStateProvider : PreviewParameterProvider<SpaceAnnouncementState> {
override val values: Sequence<SpaceAnnouncementState>
get() = sequenceOf(
aSpaceAnnouncementState(),
)
}
fun aSpaceAnnouncementState(
eventSink: (SpaceAnnouncementEvents) -> Unit = {},
) = SpaceAnnouncementState(
eventSink = eventSink,
)

View file

@ -1,158 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.announcement.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181
*/
@Composable
fun SpaceAnnouncementView(
state: SpaceAnnouncementState,
modifier: Modifier = Modifier,
) {
val eventSink = state.eventSink
fun onContinue() {
eventSink(SpaceAnnouncementEvents.Continue)
}
BackHandler(onBack = ::onContinue)
HeaderFooterPage(
modifier = modifier,
isScrollable = true,
contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp),
header = {
SpaceAnnouncementHeader()
},
content = {
SpaceAnnouncementContent(
modifier = Modifier.padding(horizontal = 8.dp),
)
},
footer = {
SpaceAnnouncementFooter(
onContinue = ::onContinue,
)
}
)
}
@Composable
private fun SpaceAnnouncementHeader(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 16.dp, bottom = 16.dp),
title = stringResource(id = R.string.screen_space_announcement_title),
showBetaLabel = true,
subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.SpaceSolid(),
usePrimaryTint = true,
),
)
}
@Composable
private fun SpaceAnnouncementContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
InfoListOrganism(
modifier = Modifier.fillMaxWidth(),
items = persistentListOf(
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item1),
iconVector = CompoundIcons.VisibilityOn(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item2),
iconVector = CompoundIcons.Email(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item3),
iconVector = CompoundIcons.Search(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item4),
iconVector = CompoundIcons.Explore(),
),
InfoListItem(
message = stringResource(id = R.string.screen_space_announcement_item5),
iconVector = CompoundIcons.Leave(),
),
),
textStyle = ElementTheme.typography.fontBodyLgMedium,
iconTint = ElementTheme.colors.iconSecondary,
iconSize = 24.dp
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
text = stringResource(id = R.string.screen_space_announcement_notice),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun SpaceAnnouncementFooter(
onContinue: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 8.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
)
}
}
@PreviewsDayNight
@Composable
internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview {
SpaceAnnouncementView(
state = state,
)
}

View file

@ -35,9 +35,10 @@ class DefaultAnnouncementStore(
override fun announcementStatusFlow(announcement: Announcement): Flow<AnnouncementStatus> {
val key = announcement.toKey()
// Announcement.Fullscreen.Space is disabled, consider it's shown
// For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08)
val defaultStatus = when (announcement) {
Announcement.Space -> AnnouncementStatus.NeverShown
Announcement.Fullscreen.Space -> AnnouncementStatus.Shown
Announcement.NewNotificationSound -> AnnouncementStatus.Shown
}
return store.data.map { prefs ->
@ -52,6 +53,6 @@ class DefaultAnnouncementStore(
}
private fun Announcement.toKey() = when (this) {
Announcement.Space -> spaceAnnouncementKey
Announcement.Fullscreen.Space -> spaceAnnouncementKey
Announcement.NewNotificationSound -> newNotificationSoundKey
}

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_space_announcement_item1">"作成または参加したスペースを表示できます"</string>
<string name="screen_space_announcement_item2">"スペースへの招待を受諾または拒否できます"</string>
<string name="screen_space_announcement_item3">"スペース内の参加可能なルームを検索できます"</string>
<string name="screen_space_announcement_item4">"公開スペースに参加できます"</string>
<string name="screen_space_announcement_item5">"参加したスペースを退出できます"</string>
<string name="screen_space_announcement_notice">"スペースの作成や管理, フィルター検索は近日実装予定です。"</string>
<string name="screen_space_announcement_subtitle">"ベータ版のスペースにようこそ。この最新のバージョンでは:"</string>
<string name="screen_space_announcement_title">"スペースの紹介"</string>
</resources>

View file

@ -6,6 +6,6 @@
<string name="screen_space_announcement_item4">"加入公共空间"</string>
<string name="screen_space_announcement_item5">"离开你加入的所有空间"</string>
<string name="screen_space_announcement_notice">"筛选、创建及管理空间功能即将上线。"</string>
<string name="screen_space_announcement_subtitle">"欢迎使用 Spaces 测试版!使用首个版本,您可以:"</string>
<string name="screen_space_announcement_title">"Spaces 简介"</string>
<string name="screen_space_announcement_subtitle">"欢迎使用空间测试版!使用首个版本,您可以:"</string>
<string name="screen_space_announcement_title">"空间简介"</string>
</resources>

View file

@ -14,6 +14,7 @@ import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -23,25 +24,47 @@ class AnnouncementPresenterTest {
val presenter = createAnnouncementPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
assertThat(state.announcement).isNull()
}
}
@Test
fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest {
fun `present - showFullscreen value depends on the value in the store`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
val state = awaitItem()
assertThat(state.showSpaceAnnouncement).isFalse()
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.showSpaceAnnouncement).isTrue()
store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.showSpaceAnnouncement).isFalse()
assertThat(finalState.announcement).isNull()
}
}
@Test
fun `present - continue event will mark the announcement as Shown`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
val state = awaitItem()
assertThat(state.announcement).isNull()
store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show)
val statusShow = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShow).isEqualTo(AnnouncementStatus.Show)
val updatedState = awaitItem()
assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space)
updatedState.eventSink(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
val statusShown = store.announcementStatusFlow(Announcement.Fullscreen.Space).first()
assertThat(statusShown).isEqualTo(AnnouncementStatus.Shown)
val finalState = awaitItem()
assertThat(finalState.announcement).isNull()
}
}
}

View file

@ -11,31 +11,28 @@ package io.element.android.features.announcement.impl
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState
import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultAnnouncementServiceTest {
@Test
fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest {
fun `when showing Fullscreen announcement, Fullscreen announcement is set to show only if it was never shown`() = runTest {
val announcementStore = InMemoryAnnouncementStore()
val sut = createDefaultAnnouncementService(
announcementStore = announcementStore,
)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Show)
// Simulate user close the announcement
sut.onAnnouncementDismissed(Announcement.Space)
sut.onAnnouncementDismissed(Announcement.Fullscreen.Space)
// Entering again the space tab should not change the value
sut.showAnnouncement(Announcement.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
sut.showAnnouncement(Announcement.Fullscreen.Space)
assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
@Test
@ -62,11 +59,7 @@ class DefaultAnnouncementServiceTest {
)
sut.announcementsToShowFlow().test {
assertThat(awaitItem()).isEmpty()
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show)
assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown)
assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound)
announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown)
assertThat(awaitItem()).isEmpty()
@ -75,11 +68,9 @@ class DefaultAnnouncementServiceTest {
private fun createDefaultAnnouncementService(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
announcementPresenter: Presenter<AnnouncementState> = Presenter { anAnnouncementState() },
spaceAnnouncementPresenter: Presenter<SpaceAnnouncementState> = Presenter { aSpaceAnnouncementState() },
announcementPresenter: AnnouncementPresenter = AnnouncementPresenter(announcementStore),
) = DefaultAnnouncementService(
announcementStore = announcementStore,
announcementPresenter = announcementPresenter,
spaceAnnouncementPresenter = spaceAnnouncementPresenter,
)
}

View file

@ -6,12 +6,16 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
package io.element.android.features.announcement.impl.fullscreen
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.AnnouncementEvent
import io.element.android.features.announcement.impl.AnnouncementState
import io.element.android.features.announcement.impl.anAnnouncementState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
@ -22,39 +26,41 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SpaceAnnouncementViewTest {
class FullscreenAnnouncementViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on back sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
@Test
fun `clicking on Continue sends a SpaceAnnouncementEvents`() {
val eventsRecorder = EventsRecorder<SpaceAnnouncementEvents>()
rule.setSpaceAnnouncementView(
aSpaceAnnouncementState(
fun `clicking on Continue sends a AnnouncementEvent`() {
val eventsRecorder = EventsRecorder<AnnouncementEvent>()
rule.setFullscreenAnnouncementView(
anAnnouncementState(
announcement = Announcement.Fullscreen.Space,
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue)
eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceAnnouncementView(
state: SpaceAnnouncementState,
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setFullscreenAnnouncementView(
state: AnnouncementState,
) {
setContent {
SpaceAnnouncementView(
FullscreenAnnouncementView(
state = state,
)
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.announcement.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.impl.store.AnnouncementStatus
import io.element.android.features.announcement.impl.store.AnnouncementStore
import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SpaceAnnouncementPresenterTest {
@Test
fun `present - when user continues, the store is updated`() = runTest {
val store = InMemoryAnnouncementStore()
val presenter = createSpaceAnnouncementPresenter(
announcementStore = store,
)
presenter.test {
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown)
val state = awaitItem()
state.eventSink(SpaceAnnouncementEvents.Continue)
assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown)
}
}
}
private fun createSpaceAnnouncementPresenter(
announcementStore: AnnouncementStore = InMemoryAnnouncementStore(),
) = SpaceAnnouncementPresenter(
announcementStore = announcementStore,
)

View file

@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class InMemoryAnnouncementStore(
initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown,
) : AnnouncementStore {
private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus)
private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus)
private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus)
override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) {
@ -29,12 +29,12 @@ class InMemoryAnnouncementStore(
}
override suspend fun reset() {
spaceAnnouncement.value = AnnouncementStatus.NeverShown
fullScreenAnnouncement.value = AnnouncementStatus.NeverShown
newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown
}
private fun Announcement.toMutableStateFlow() = when (this) {
Announcement.Space -> spaceAnnouncement
is Announcement.Fullscreen -> fullScreenAnnouncement
Announcement.NewNotificationSound -> newNotificationSoundAnnouncement
}
}

View file

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

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"通話中"</string>
<string name="call_foreground_service_message_android">"タップして通話に戻る"</string>
<string name="call_foreground_service_title_android">"☎️ 通話中"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element CallはこのAndroidバージョンにおいて、Bluetoothオーディオデバイスの使用をサポートしていません。別のオーディオデバイスを選択してください。"</string>
<string name="screen_incoming_call_subtitle_android">"Element Call の着信"</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Cuộc gọi đang diễn ra"</string>
<string name="call_foreground_service_message_android">"Nhấn để quay lại cuộc gọi."</string>
<string name="call_foreground_service_title_android">"☎️ Cuộc gọi đang diễn ra"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Ứng dụng Element Call không hỗ trợ sử dụng thiết bị âm thanh Bluetooth trên phiên bản Android này. Vui lòng chọn thiết bị âm thanh khác."</string>
<string name="screen_incoming_call_subtitle_android">"Cuộc gọi Element đến"</string>
</resources>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,14 +3,34 @@
<string name="screen_create_room_action_create_room">"Nuova stanza"</string>
<string name="screen_create_room_add_people_title">"Invita persone"</string>
<string name="screen_create_room_error_creating_room">"Si è verificato un errore durante la creazione della stanza"</string>
<string name="screen_create_room_private_option_description">"Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end."</string>
<string name="screen_create_room_error_creating_space">"Non è stato possibile creare lo spazio a causa di un errore sconosciuto. Riprova più tardi."</string>
<string name="screen_create_room_name_placeholder">"Aggiungi nome…"</string>
<string name="screen_create_room_new_room_title">"Nuova stanza"</string>
<string name="screen_create_room_new_space_title">"Nuovo spazio"</string>
<string name="screen_create_room_private_option_description">"Possono partecipare solo le persone invitate."</string>
<string name="screen_create_room_private_option_title">"Privato"</string>
<string name="screen_create_room_public_option_description">"Chiunque può trovare questa stanza.
Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Chiedi di entrare"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Chiunque può entrare in questa stanza"</string>
<string name="screen_create_room_room_address_section_footer">"Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo della stanza"</string>
<string name="screen_create_room_public_option_short_description">"Chiunque può partecipare."</string>
<string name="screen_create_room_public_option_title">"Pubblico"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Chiunque può chiedere di partecipare, ma un amministratore o un moderatore deve accettare la richiesta."</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Consenti di chiedere di partecipare"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"Chiunque sia membro di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso."</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"Richiedi accesso"</string>
<string name="screen_create_room_room_access_section_private_option_description">"Possono partecipare solo le persone invitate."</string>
<string name="screen_create_room_room_access_section_private_option_title">"Privato"</string>
<string name="screen_create_room_room_access_section_public_option_description">"Chiunque può partecipare."</string>
<string name="screen_create_room_room_access_section_public_option_title">"Pubblico"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"Chiunque in %1$s può unirsi."</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"Standard"</string>
<string name="screen_create_room_room_access_section_title">"Chi ha accesso"</string>
<string name="screen_create_room_room_address_section_footer">"Avrai bisogno di un indirizzo per renderlo visibile nella directory pubblica."</string>
<string name="screen_create_room_room_address_section_title">"Indirizzo"</string>
<string name="screen_create_room_room_visibility_section_title">"Visibilità della stanza"</string>
<string name="screen_create_room_space_selection_no_space_description">"(nessuno spazio)"</string>
<string name="screen_create_room_space_selection_no_space_option">"Non aggiungere a uno spazio"</string>
<string name="screen_create_room_space_selection_no_space_title">"Nessuno spazio selezionato"</string>
<string name="screen_create_room_space_selection_sheet_title">"Aggiungi allo spazio"</string>
<string name="screen_create_room_topic_label">"Argomento (facoltativo)"</string>
<string name="screen_create_room_topic_placeholder">"Aggiungi descrizione…"</string>
</resources>

View file

@ -0,0 +1,36 @@
<?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_error_creating_space">"不明な問題のためスペースを作成できませんでした。再度お試しください。"</string>
<string name="screen_create_room_name_placeholder">"名前を追加…"</string>
<string name="screen_create_room_new_room_title">"新しいルーム"</string>
<string name="screen_create_room_new_space_title">"新しいスペース"</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_short_description">"誰でも参加できます。"</string>
<string name="screen_create_room_public_option_title">"公開"</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"誰でも参加できますが、管理者またはモデレーターの承認が必要です。"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"参加の要求を許可"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_description">"%1$s にいる全員が参加することができますが、事前に参加の要求をする必要があります。"</string>
<string name="screen_create_room_room_access_section_knocking_restricted_option_title">"参加を要求"</string>
<string name="screen_create_room_room_access_section_private_option_description">"招待されたユーザーのみが参加できます。"</string>
<string name="screen_create_room_room_access_section_private_option_title">"非公開"</string>
<string name="screen_create_room_room_access_section_public_option_description">"誰でも参加できます。"</string>
<string name="screen_create_room_room_access_section_public_option_title">"公開"</string>
<string name="screen_create_room_room_access_section_restricted_option_description">"%1$s にいる全員が参加することができます。"</string>
<string name="screen_create_room_room_access_section_restricted_option_title">"スタンダード"</string>
<string name="screen_create_room_room_access_section_title">"参加できるユーザー"</string>
<string name="screen_create_room_room_address_section_footer">"公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。"</string>
<string name="screen_create_room_room_address_section_title">"アドレス"</string>
<string name="screen_create_room_room_visibility_section_title">"ルームの公開度"</string>
<string name="screen_create_room_space_selection_no_space_description">"(スペースなし)"</string>
<string name="screen_create_room_space_selection_no_space_option">"スペースに追加しない"</string>
<string name="screen_create_room_space_selection_no_space_title">"スペースが選択されていません"</string>
<string name="screen_create_room_space_selection_sheet_title">"スペースに追加"</string>
<string name="screen_create_room_topic_label">"トピック (任意)"</string>
<string name="screen_create_room_topic_placeholder">"説明を追加…"</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_create_room_action_create_room">"Phòng mới"</string>
<string name="screen_create_room_add_people_title">"Mời ai đó"</string>
<string name="screen_create_room_error_creating_room">"Đã xảy ra lỗi khi tạo phòng."</string>
<string name="screen_create_room_private_option_description">"Chỉ những người được mời mới có thể tham gia."</string>
<string name="screen_create_room_public_option_description">"Bất kỳ ai cũng có thể tìm thấy phòng này.
Bạn có thể thay đổi cài đặt phòng bất cứ lúc nào."</string>
<string name="screen_create_room_topic_label">"Chủ đề (tùy chọn)"</string>
<string name="screen_create_room_topic_placeholder">"Thêm mô tả…"</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_deactivate_account_confirmation_dialog_content">"アカウントを無効化することを再度確認します。この操作は元に戻せません。"</string>
<string name="screen_deactivate_account_delete_all_messages">"メッセージをすべて削除"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"注意: 新しいユーザーには断片的な会話が表示されます"</string>
<string name="screen_deactivate_account_description">"アカウントを無効化することは %1$s であり、次の変化が生じます:"</string>
<string name="screen_deactivate_account_description_bold_part">"不可逆"</string>
<string name="screen_deactivate_account_list_item_1">"アカウントを %1$s (再度ログイン不可, 同一のIDを再利用不可)"</string>
<string name="screen_deactivate_account_list_item_1_bold_part">"恒久的に無効化する"</string>
<string name="screen_deactivate_account_list_item_2">"すべてのチャットルームから退出します。"</string>
<string name="screen_deactivate_account_list_item_3">"アカウント提供元サーバーからアカウント情報を削除します。"</string>
<string name="screen_deactivate_account_list_item_4">"あなたの会話は、既存ユーザーには引き続き表示されますが、新規ユーザーには表示されなくなります。"</string>
<string name="screen_deactivate_account_title">"アカウントを無効化"</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_deactivate_account_delete_all_messages">"Xóa tất cả tin nhắn của tôi"</string>
<string name="screen_deactivate_account_delete_all_messages_notice">"Cảnh báo: Người dùng sau này có thể thấy các cuộc trò chuyện chưa hoàn chỉnh."</string>
<string name="screen_deactivate_account_list_item_4">"Tin nhắn của bạn vẫn sẽ hiển thị cho người dùng đã đăng ký nhưng sẽ không hiển thị cho người dùng mới hoặc chưa đăng ký nếu bạn chọn xóa chúng."</string>
<string name="screen_deactivate_account_title">"Vô hiệu hóa tài khoản"</string>
</resources>

View file

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

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Kan ikke bekræfte?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Opret en ny gendannelsesnøgle"</string>
<string name="screen_identity_confirmation_subtitle">"Verificér denne enhed for at konfigurere sikre meddelelser."</string>
<string name="screen_identity_confirmation_title">"Bekræft din identitet"</string>
<string name="screen_identity_confirmation_subtitle">"Vælg, hvordan du vil verificere dig for at konfigurere sikre beskeder."</string>
<string name="screen_identity_confirmation_title">"Bekræft din digitale identitet"</string>
<string name="screen_identity_confirmation_use_another_device">"Brug en anden enhed"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Brug gendannelsesnøgle"</string>
<string name="screen_identity_confirmed_subtitle">"Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed."</string>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Non puoi confermare?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Crea una nuova chiave di recupero"</string>
<string name="screen_identity_confirmation_subtitle">"Verifica questo dispositivo per segnare i tuoi messaggi come sicuri."</string>
<string name="screen_identity_confirmation_title">"Conferma la tua identità"</string>
<string name="screen_identity_confirmation_subtitle">"Scegli come effettuare la verifica per configurare la messaggistica sicura."</string>
<string name="screen_identity_confirmation_title">"Conferma la tua identità digitale"</string>
<string name="screen_identity_confirmation_use_another_device">"Usa un altro dispositivo"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Usa la chiave di recupero"</string>
<string name="screen_identity_confirmed_subtitle">"Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo."</string>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"認証できませんか?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"回復鍵を新規作成します"</string>
<string name="screen_identity_confirmation_subtitle">"安全なメッセージを設定するための検証方法を選択してください。"</string>
<string name="screen_identity_confirmation_title">"デジタルIDの認証"</string>
<string name="screen_identity_confirmation_use_another_device">"他の端末を使用"</string>
<string name="screen_identity_confirmation_use_recovery_key">"回復鍵を使用"</string>
<string name="screen_identity_confirmed_subtitle">"メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。"</string>
<string name="screen_identity_confirmed_title">"検証済みの端末"</string>
<string name="screen_identity_use_another_device">"他の端末を使用"</string>
<string name="screen_identity_waiting_on_other_device">"一方の端末を待機中…"</string>
<string name="screen_notification_optin_subtitle">"設定は後で変更することができます。"</string>
<string name="screen_notification_optin_title">"メッセージを見逃さないため通知を許可"</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"Не можете подтвердить?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"Создайте новый ключ восстановления"</string>
<string name="screen_identity_confirmation_subtitle">"Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями."</string>
<string name="screen_identity_confirmation_subtitle">"Выберите способ подтверждения для настройки защищенного обмена сообщениями."</string>
<string name="screen_identity_confirmation_title">"Подтвердите личность"</string>
<string name="screen_identity_confirmation_use_another_device">"Использовать другое устройство"</string>
<string name="screen_identity_confirmation_use_recovery_key">"Использовать ключ восстановления"</string>

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_identity_confirmation_subtitle">"Chọn phương thức xác minh để bật nhắn tin bảo mật."</string>
<string name="screen_identity_confirmation_title">"Xác nhận danh tính kỹ thuật số của bạn"</string>
<string name="screen_identity_confirmed_subtitle">"Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này."</string>
<string name="screen_identity_confirmed_title">"Thiết bị được xác thực"</string>
<string name="screen_identity_waiting_on_other_device">"Đang chờ trên thiết bị khác…"</string>
<string name="screen_notification_optin_subtitle">"Bạn có thể thay đổi cài đặt sau."</string>
<string name="screen_notification_optin_title">"Cho phép thông báo để không bỏ lỡ bất kỳ tin nhắn nào"</string>
</resources>

View file

@ -2,8 +2,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmation_cannot_confirm">"无法确认?"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"创建新的恢复密钥"</string>
<string name="screen_identity_confirmation_subtitle">"验证此设备以开始安全地收发消息。"</string>
<string name="screen_identity_confirmation_title">"确认这是你"</string>
<string name="screen_identity_confirmation_subtitle">"选择验证方式以设置安全的消息传输。"</string>
<string name="screen_identity_confirmation_title">"确认您的数字身份"</string>
<string name="screen_identity_confirmation_use_another_device">"使用其他设备"</string>
<string name="screen_identity_confirmation_use_recovery_key">"使用恢复密钥"</string>
<string name="screen_identity_confirmed_subtitle">"现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。"</string>

View file

@ -19,8 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
@ -47,7 +45,6 @@ class HomePresenter(
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val sessionStore: SessionStore,
private val announcementService: AnnouncementService,
) : Presenter<HomeState> {
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
@ -82,10 +79,7 @@ class HomePresenter(
fun handleEvent(event: HomeEvent) {
when (event) {
is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch {
if (event.item == HomeNavigationBarItem.Spaces) {
announcementService.showAnnouncement(Announcement.Space)
}
is HomeEvent.SelectHomeNavigationBarItem -> {
currentHomeNavigationBarItemOrdinal = event.item.ordinal
}
is HomeEvent.SwitchToAccount -> coroutineState.launch {
@ -94,12 +88,6 @@ class HomePresenter(
}
}
LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) {
// If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered.
if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) {
currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal
}
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
return HomeState(
currentUserAndNeighbors = currentUserAndNeighbors,

View file

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

View file

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

View file

@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
@ -349,6 +350,7 @@ private fun MessagePreviewAndIndicatorRow(
if (room.hasRoomCall) {
OnGoingCallIcon(
color = tint,
isAudio = room.activeCallIntent == CallIntent.AUDIO
)
}
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
@ -398,10 +400,11 @@ private fun InviteNameAndIndicatorRow(
@Composable
private fun OnGoingCallIcon(
color: Color,
isAudio: Boolean
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.VideoCallSolid(),
imageVector = if (isAudio) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call),
tint = color,
)

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
@ -50,6 +51,11 @@ class RoomListRoomSummaryFactory(
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
activeCallIntent = when (val consensus = roomInfo.activeCallIntentConsensus) {
is CallIntentConsensus.Full -> consensus.callIntent
is CallIntentConsensus.Partial -> consensus.callIntent
CallIntentConsensus.None -> null
},
isDirect = roomInfo.isDirect,
isFavorite = roomInfo.isFavorite,
inviteSender = roomInfo.inviter?.toInviteSender(),

View file

@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
@ -33,6 +34,7 @@ data class RoomListRoomSummary(
val avatarData: AvatarData,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val activeCallIntent: CallIntent?,
val isDirect: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
@ -132,6 +133,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
listOf(
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
),
listOf(
aRoomListRoomSummary(
name = "Active voice call",
latestEvent = LatestEvent.Synced("No activity, call"),
hasRoomCall = true,
activeCallIntent = CallIntent.AUDIO
),
)
).flatten()
}
@ -158,6 +167,7 @@ internal fun aRoomListRoomSummary(
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
activeCallIntent: CallIntent? = null,
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
isDirect: Boolean = false,
isDm: Boolean = false,
@ -181,6 +191,7 @@ internal fun aRoomListRoomSummary(
avatarData = avatarData,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
activeCallIntent = activeCallIntent,
isDirect = isDirect,
isDm = isDm,
isFavorite = isFavorite,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,9 @@
<string name="banner_battery_optimization_title_android">"Le notifiche non arrivano?"</string>
<string name="banner_new_sound_message">"Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso."</string>
<string name="banner_new_sound_title">"Abbiamo rinnovato i tuoi suoni"</string>
<string name="banner_set_up_recovery_content">"Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi."</string>
<string name="banner_set_up_recovery_submit">"Configura il recupero"</string>
<string name="banner_set_up_recovery_title">"Configura il ripristino"</string>
<string name="banner_set_up_recovery_content">"Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero."</string>
<string name="banner_set_up_recovery_submit">"Ottieni la chiave di recupero"</string>
<string name="banner_set_up_recovery_title">"Esegui il backup delle tue conversazioni"</string>
<string name="confirm_recovery_key_banner_message">"Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Inserisci la tua chiave di recupero"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Hai dimenticato la chiave di recupero?"</string>
@ -50,6 +50,7 @@ Non hai messaggi non letti!"</string>
<string name="screen_roomlist_mark_as_read">"Segna come letto"</string>
<string name="screen_roomlist_mark_as_unread">"Segna come non letto"</string>
<string name="screen_roomlist_tombstoned_room_description">"Questa stanza è stata aggiornata"</string>
<string name="screen_roomlist_your_spaces">"I tuoi spazi"</string>
<string name="session_verification_banner_message">"Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati."</string>
<string name="session_verification_banner_title">"Verifica che sei tu"</string>
</resources>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。"</string>
<string name="banner_battery_optimization_submit_android">"最適化を無効にする"</string>
<string name="banner_battery_optimization_title_android">"通知が届いていませんか?"</string>
<string name="banner_new_sound_message">"通知音が更新され、より明確で速く、そして邪魔にならなくなりました。"</string>
<string name="banner_new_sound_title">"サウンドを刷新しました"</string>
<string name="banner_set_up_recovery_content">"あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。"</string>
<string name="banner_set_up_recovery_submit">"回復鍵を作成"</string>
<string name="banner_set_up_recovery_title">"チャットをバックアップ"</string>
<string name="confirm_recovery_key_banner_message">"鍵の保管庫と過去のメッセージにアクセスするために、回復鍵を認証してください。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"回復鍵を入力してください"</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"回復鍵を忘れましたか?"</string>
<string name="confirm_recovery_key_banner_title">"鍵の保管庫を同期できません。"</string>
<string name="full_screen_intent_banner_message">"重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。"</string>
<string name="full_screen_intent_banner_title">"通話品質を高める"</string>
<string name="screen_home_tab_chats">"チャット"</string>
<string name="screen_home_tab_spaces">"スペース"</string>
<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>
<string name="screen_migration_message">"一度限りの工程です。お待ちください。"</string>
<string name="screen_migration_title">"アカウントを設定しています。"</string>
<string name="screen_roomlist_a11y_create_message">"新しい会話またはルームを作成"</string>
<string name="screen_roomlist_clear_filters">"フィルターを解除"</string>
<string name="screen_roomlist_empty_message">"誰かにメッセージを送信しましょう。"</string>
<string name="screen_roomlist_empty_title">"まだチャットがありません。"</string>
<string name="screen_roomlist_filter_favourites">"お気に入り"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"チャットの設定からお気に入りに追加できます。
現在は、フィルターの選択を解除することで他のチャットを表示できます。"</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"お気に入りのチャットはまだありません"</string>
<string name="screen_roomlist_filter_invites">"招待"</string>
<string name="screen_roomlist_filter_invites_empty_state_title">"承認待ちの招待はありません"</string>
<string name="screen_roomlist_filter_low_priority">"低い優先度"</string>
<string name="screen_roomlist_filter_low_priority_empty_state_title">"低い優先度のチャットはまだありません"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"フィルターを解除して他のチャットを表示できます"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"この選択中にチャットがありません"</string>
<string name="screen_roomlist_filter_people">"人"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"まだダイレクトメッセージは届いていません"</string>
<string name="screen_roomlist_filter_rooms">"ルーム"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"まだルームに参加していません"</string>
<string name="screen_roomlist_filter_unreads">"未読"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"やった!
未読メッセージはありません。"</string>
<string name="screen_roomlist_knock_event_sent_description">"参加リクエストを送信しました"</string>
<string name="screen_roomlist_main_space_title">"チャット"</string>
<string name="screen_roomlist_mark_as_read">"既読にする"</string>
<string name="screen_roomlist_mark_as_unread">"未読にする"</string>
<string name="screen_roomlist_tombstoned_room_description">"このルームはアップグレードされました"</string>
<string name="screen_roomlist_your_spaces">"あなたのスペース"</string>
<string name="session_verification_banner_message">"新しいデバイスをご利用のようです。暗号化されたメッセージにアクセスするには、別のデバイスで検証してください。"</string>
<string name="session_verification_banner_title">"本人確認"</string>
</resources>

View file

@ -5,7 +5,7 @@
<string name="banner_battery_optimization_title_android">"Уведомления не приходят?"</string>
<string name="banner_new_sound_message">"Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие."</string>
<string name="banner_new_sound_title">"Мы обновили ваши звуки"</string>
<string name="banner_set_up_recovery_content">"Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам."</string>
<string name="banner_set_up_recovery_content">"Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления."</string>
<string name="banner_set_up_recovery_submit">"Получить ключ восстановления"</string>
<string name="banner_set_up_recovery_title">"Сделайте резервную копию своих чатов."</string>
<string name="confirm_recovery_key_banner_message">"Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений."</string>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Tắt tính năng tối ưu hóa pin cho ứng dụng này để đảm bảo nhận được mọi thông báo."</string>
<string name="banner_battery_optimization_submit_android">"Tắt tối ưu hóa"</string>
<string name="banner_battery_optimization_title_android">"Có nhận được thông báo không?"</string>
<string name="banner_new_sound_message">"Thông báo của bạn đã được cập nhật — rõ ràng hơn, nhanh hơn và ít gây khó chịu hơn."</string>
<string name="banner_new_sound_title">"Chúng tôi đã làm mới âm thanh của bạn."</string>
<string name="banner_set_up_recovery_content">"Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục."</string>
<string name="banner_set_up_recovery_submit">"Lấy khóa khôi phục."</string>
<string name="banner_set_up_recovery_title">"Sao lưu tin nhắn của bạn"</string>
<string name="confirm_recovery_key_banner_message">"Xác nhận khóa khôi phục để không bị mất quyền truy cập vào tin nhắn."</string>
<string name="confirm_recovery_key_banner_primary_button_title">"Nhập khóa khôi phục của bạn."</string>
<string name="confirm_recovery_key_banner_secondary_button_title">"Bạn quên khóa khôi phục?”"</string>
<string name="confirm_recovery_key_banner_title">"Dữ liệu khóa của bạn không còn đồng bộ"</string>
<string name="screen_home_tab_chats">"Cuộc trò chuyện"</string>
<string name="screen_invites_decline_chat_message">"Bạn có chắc muốn từ chối lời mời tham gia %1$s không?"</string>
<string name="screen_invites_decline_chat_title">"Từ chối lời mời"</string>
<string name="screen_invites_decline_direct_chat_message">"Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?"</string>
<string name="screen_invites_decline_direct_chat_title">"Từ chối trò chuyện"</string>
<string name="screen_invites_empty_list">"Không có lời mời"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) đã mời bạn"</string>
<string name="screen_migration_message">"Quá trình này chỉ thực hiện một lần, cảm ơn bạn đã kiên nhẫn."</string>
<string name="screen_migration_title">"Đang thiết lập tài khoản của bạn."</string>
<string name="screen_roomlist_a11y_create_message">"Tạo một cuộc trò chuyện hoặc phòng mới"</string>
<string name="screen_roomlist_empty_message">"Bắt đầu bằng cách nhắn tin cho ai đó."</string>
<string name="screen_roomlist_empty_title">"Chưa có cuộc trò chuyện nào."</string>
<string name="screen_roomlist_filter_favourites">"Yêu thích"</string>
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Bạn có thể thêm cuộc trò chuyện vào mục yêu thích trong cài đặt chat.
Hiện tại, bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác."</string>
<string name="screen_roomlist_filter_favourites_empty_state_title">"Bạn chưa có cuộc trò chuyện yêu thích nào."</string>
<string name="screen_roomlist_filter_invites">"Lời mời"</string>
<string name="screen_roomlist_filter_low_priority">"Ưu tiên thấp"</string>
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác"</string>
<string name="screen_roomlist_filter_mixed_empty_state_title">"Bạn không có cuộc trò chuyện nào cho lựa chọn này"</string>
<string name="screen_roomlist_filter_people">"Danh bạ"</string>
<string name="screen_roomlist_filter_people_empty_state_title">"Bạn chưa có tin nhắn riêng nào cả"</string>
<string name="screen_roomlist_filter_rooms">"Phòng"</string>
<string name="screen_roomlist_filter_rooms_empty_state_title">"Bạn chưa tham gia phòng nào"</string>
<string name="screen_roomlist_filter_unreads">"Chưa đọc"</string>
<string name="screen_roomlist_filter_unreads_empty_state_title">"Chúc mừng!
Bạn không còn tin nhắn nào chưa đọc nữa!"</string>
<string name="screen_roomlist_knock_event_sent_description">"Yêu cầu tham gia đã được gửi"</string>
<string name="screen_roomlist_main_space_title">"Cuộc trò chuyện"</string>
<string name="screen_roomlist_mark_as_read">"Đánh dấu đã đọc"</string>
<string name="screen_roomlist_mark_as_unread">"Đánh dấu chưa đọc"</string>
<string name="session_verification_banner_message">"Có vẻ như bạn đang sử dụng thiết bị mới. Hãy xác minh bằng một thiết bị khác để truy cập tin nhắn được mã hóa của bạn."</string>
<string name="session_verification_banner_title">"Xác thực danh tính của bạn"</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="banner_new_sound_message">"您的通知提示音已升级 - 更清晰、更快速、干扰更少。"</string>
<string name="banner_new_sound_title">"我们已更新您的声音"</string>
<string name="banner_set_up_recovery_content">"生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。"</string>
<string name="banner_set_up_recovery_submit">"设置恢复"</string>
<string name="banner_set_up_recovery_submit">"获取恢复密钥"</string>
<string name="banner_set_up_recovery_title">"设置恢复"</string>
<string name="confirm_recovery_key_banner_message">"确认恢复密钥,以保持对密钥存储和消息历史的访问。"</string>
<string name="confirm_recovery_key_banner_primary_button_title">"输入恢复密钥"</string>

View file

@ -9,14 +9,11 @@
package io.element.android.features.home.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.indicator.api.IndicatorService
@ -33,10 +30,7 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -79,7 +73,6 @@ class HomePresenterTest {
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
)
assertThat(withUserState.showAvatarIndicator).isFalse()
assertThat(withUserState.showNavigationBar).isTrue()
}
}
@ -139,14 +132,10 @@ class HomePresenterTest {
@Test
fun `present - NavigationBar change`() = runTest {
val showAnnouncementResult = lambdaRecorder<Announcement, Unit> { }
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
announcementService = FakeAnnouncementService(
showAnnouncementResult = showAnnouncementResult,
)
)
presenter.test {
val initialState = awaitItem()
@ -154,38 +143,6 @@ class HomePresenterTest {
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val finalState = awaitItem()
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
showAnnouncementResult.assertions().isCalledOnce()
.with(value(Announcement.Space))
}
}
@Test
fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest {
val homeSpacesPresenter = MutablePresenter(aHomeSpacesState())
val presenter = createHomePresenter(
sessionStore = InMemorySessionStore(
updateUserProfileResult = { _, _, _ -> },
),
homeSpacesPresenter = homeSpacesPresenter,
announcementService = FakeAnnouncementService(
showAnnouncementResult = {},
)
)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(initialState.showNavigationBar).isTrue()
// User navigate to Spaces
initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
val spaceState = awaitItem()
assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces)
// The last space is left
homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false))
skipItems(1)
val finalState = awaitItem()
// We are back to Chats
assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
assertThat(finalState.showNavigationBar).isFalse()
}
}
}
@ -198,7 +155,6 @@ internal fun createHomePresenter(
indicatorService: IndicatorService = FakeIndicatorService(),
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
sessionStore: SessionStore = InMemorySessionStore(),
announcementService: AnnouncementService = FakeAnnouncementService(),
) = HomePresenter(
client = client,
syncService = syncService,
@ -209,5 +165,4 @@ internal fun createHomePresenter(
logoutPresenter = { aDirectLogoutState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
sessionStore = sessionStore,
announcementService = announcementService,
)

View file

@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary(
displayType = displayType,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = false,
activeCallIntent = null,
isDirect = false,
isFavorite = isFavorite,
canonicalAlias = null,

View file

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

View file

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

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_title">"Заблакіраваць карыстальніка"</string>
<string name="screen_decline_and_block_title">"Адхіліць і заблакіраваць"</string>
<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>
<string name="screen_join_room_decline_and_block_button_title">"Адхіліць і заблакіраваць"</string>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"このユーザーからのメッセージと招待を非表示します"</string>
<string name="screen_decline_and_block_block_user_option_title">"ユーザーをブロック"</string>
<string name="screen_decline_and_block_report_user_option_description">"アカウント提供元にこのルームを報告"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"報告の理由を説明してください…"</string>
<string name="screen_decline_and_block_title">"拒否してブロック"</string>
<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>
<string name="screen_join_room_decline_and_block_alert_confirmation">"拒否してブロックする"</string>
<string name="screen_join_room_decline_and_block_alert_message">"本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"招待を拒否してブロック"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒否してブロック"</string>
</resources>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"Bạn sẽ không nhận được bất kỳ tin nhắn hoặc lời mời tham gia phòng nào từ người dùng này."</string>
<string name="screen_decline_and_block_block_user_option_title">"Chặn người dùng"</string>
<string name="screen_decline_and_block_report_user_option_description">"Báo cáo phòng này cho nhà cung cấp tài khoản của bạn."</string>
<string name="screen_decline_and_block_title">"Từ chối và chặn"</string>
<string name="screen_invites_decline_chat_message">"Bạn có chắc muốn từ chối lời mời tham gia %1$s không?"</string>
<string name="screen_invites_decline_chat_title">"Từ chối lời mời"</string>
<string name="screen_invites_decline_direct_chat_message">"Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?"</string>
<string name="screen_invites_decline_direct_chat_title">"Từ chối trò chuyện"</string>
<string name="screen_invites_empty_list">"Không có lời mời"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s ) đã mời bạn"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Có, từ chối &amp; chặn"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Từ chối lời mời và chặn"</string>
<string name="screen_join_room_decline_and_block_button_title">"Từ chối và chặn"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_decline_and_block_block_user_option_description">"您不会看到来自该用户的任何信息或房间邀请"</string>
<string name="screen_decline_and_block_block_user_option_title">"封禁用户"</string>
<string name="screen_decline_and_block_block_user_option_description">"您不会看到来自该用户的任何信息或房间邀请"</string>
<string name="screen_decline_and_block_block_user_option_title">"屏蔽用户"</string>
<string name="screen_decline_and_block_report_user_option_description">"向您的帐户提供商举报此房间。"</string>
<string name="screen_decline_and_block_report_user_reason_placeholder">"描述举报的原因…"</string>
<string name="screen_decline_and_block_title">"拒绝并屏蔽"</string>
@ -12,7 +12,7 @@
<string name="screen_invites_empty_list">"没有邀请"</string>
<string name="screen_invites_invited_you">"%1$s %2$s邀请了你"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"是的,拒绝并屏蔽"</string>
<string name="screen_join_room_decline_and_block_alert_message">"您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"</string>
<string name="screen_join_room_decline_and_block_alert_message">"您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"拒绝邀请并屏蔽"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒绝并屏蔽"</string>
</resources>

View file

@ -34,13 +34,15 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.usersearch.impl)
implementation(projects.libraries.usersearch.api)
implementation(libs.coil.compose)
implementation(projects.services.apperror.api)
implementation(projects.libraries.featureflag.api)
api(projects.features.invitepeople.api)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.libraries.featureflag.test)
}

View file

@ -5,11 +5,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.screens.onboarding.classic
package io.element.android.features.invitepeople.impl
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
class ConfirmingLoginWithElementClassic(
val userId: UserId,
data class ConfirmingUnknownUserInvitation(
val users: ImmutableList<MatrixUser>
) : AsyncAction.Confirming

View file

@ -14,4 +14,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
data object DismissUnknownUsersModal : DefaultInvitePeopleEvents
data object RemoveUnknownUsers : DefaultInvitePeopleEvents
}

View file

@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -36,8 +37,11 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -50,6 +54,7 @@ import io.element.android.services.apperror.api.AppErrorStateService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn
@ -69,6 +74,7 @@ class DefaultInvitePeoplePresenter(
private val coroutineDispatchers: CoroutineDispatchers,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val appErrorStateService: AppErrorStateService,
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient,
) : InvitePeoplePresenter {
@AssistedFactory
@ -87,6 +93,8 @@ class DefaultInvitePeoplePresenter(
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
if (roomMembers.value.isSuccess()) {
val activeMemberIds = roomMembers.value.dataOrNull().orEmpty()
@ -126,6 +134,40 @@ class DefaultInvitePeoplePresenter(
}
}
val selectedUserIdentities = produceState(
emptyMap<MatrixUser, IdentityState?>().toImmutableMap(),
selectedUsers.value,
enableKeyShareOnInvite,
) {
if (!enableKeyShareOnInvite) {
return@produceState
}
val selected = selectedUsers.value
val cached = value
.filterKeys { it in selected }
val uncached = selected
.filterNot(cached::containsKey)
.associateWith { user ->
matrixClient.encryptionService
.getUserIdentity(user.userId, fallbackToServer = false)
.getOrNull()
}
value = (cached + uncached).toImmutableMap()
}
val unknownUsers by remember {
derivedStateOf {
selectedUserIdentities.value
.filterValues { it == null }
.keys
.toImmutableList()
}
}
LaunchedEffect(room.isSuccess()) {
room.dataOrNull()?.let {
fetchMembers(it, roomMembers)
@ -144,21 +186,41 @@ class DefaultInvitePeoplePresenter(
fun handleEvent(event: InvitePeopleEvents) {
when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
// Dedicated `when` for exhaustivity.
is DefaultInvitePeopleEvents -> when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> {
sendInvitesAction.value = AsyncAction.Uninitialized
}
is DefaultInvitePeopleEvents.RemoveUnknownUsers -> {
val usersToRemove = selectedUsers.value.filter { it in unknownUsers }
usersToRemove.forEach { user ->
selectedUsers.toggleUser(user)
searchResults.toggleUser(user)
}
sendInvitesAction.value = AsyncAction.Uninitialized
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is InvitePeopleEvents.SendInvites -> {
room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers
)
} else {
room.dataOrNull()?.let {
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
}
}
}
is InvitePeopleEvents.CloseSearch -> {

View file

@ -76,6 +76,16 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
selectedUsers = aMatrixUserList().toImmutableList(),
sendInvitesAction = AsyncAction.Loading,
),
aDefaultInvitePeopleState(
sendInvitesAction = ConfirmingUnknownUserInvitation(persistentListOf(
aMatrixUser("@alice:server.org")
))
),
aDefaultInvitePeopleState(
sendInvitesAction = ConfirmingUnknownUserInvitation(
aMatrixUserList().toImmutableList()
)
),
)
}

View file

@ -16,30 +16,42 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncFailure
import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.SearchBar
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.CheckableUserRow
import io.element.android.libraries.matrix.ui.components.CheckableUserRowData
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
@ -143,6 +155,15 @@ private fun InvitePeopleContentView(
}
}
}
if (state.sendInvitesAction is ConfirmingUnknownUserInvitation) {
InvitePeopleConfirmModal(
users = state.sendInvitesAction.users,
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvents.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvents.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvents.RemoveUnknownUsers) }
)
}
}
}
@ -230,6 +251,56 @@ private fun InvitePeopleSearchBar(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun InvitePeopleConfirmModal(
users: ImmutableList<MatrixUser>,
onDismiss: () -> Unit,
onInvite: () -> Unit,
onRemove: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
dragHandle = null,
) {
IconTitleSubtitleMolecule(
title = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_title, users.size),
subTitle = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_subtitle, users.size),
iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()),
modifier = Modifier.padding(
top = 32.dp,
bottom = 16.dp,
start = 16.dp,
end = 16.dp,
)
)
LazyColumn {
items(users) { user ->
MatrixUserRow(user)
}
}
ButtonRowMolecule(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(16.dp)
) {
OutlinedButton(
text = stringResource(CommonStrings.action_remove),
onClick = onRemove,
leadingIcon = IconSource.Vector(CompoundIcons.Close()),
modifier = Modifier.weight(1f)
)
Button(
text = stringResource(CommonStrings.action_invite),
onClick = onInvite,
leadingIcon = IconSource.Vector(CompoundIcons.Check()),
modifier = Modifier.weight(1f)
)
}
}
}
@PreviewsDayNight
@Composable
internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =

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_invite_users_already_a_member">"既に参加しています"</string>
<string name="screen_invite_users_already_invited">"既に招待しています"</string>
<plurals name="screen_invite_users_confirm_dialog_subtitle">
<item quantity="other">"この連絡先とのチャットがありません。続行する前に、このルームに招待してください。"</item>
</plurals>
<plurals name="screen_invite_users_confirm_dialog_title">
<item quantity="other">"このルームに新しい連絡先を追加しますか?"</item>
</plurals>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Đã là thành viên"</string>
<string name="screen_invite_users_already_invited">"Đã được mời"</string>
</resources>

View file

@ -2,4 +2,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invite_users_already_a_member">"Already a member"</string>
<string name="screen_invite_users_already_invited">"Already invited"</string>
<plurals name="screen_invite_users_confirm_dialog_subtitle">
<item quantity="one">"You currently dont have any chats with this contact. Confirm inviting them to this room before continuing."</item>
<item quantity="other">"You currently dont have any chats with these contacts. Confirm inviting them to this room before continuing."</item>
</plurals>
<plurals name="screen_invite_users_confirm_dialog_title">
<item quantity="one">"Invite a new contact to this room?"</item>
<item quantity="other">"Invite new contacts to this room?"</item>
</plurals>
</resources>

View file

@ -15,9 +15,13 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.RoomMembersState
@ -28,6 +32,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
@ -43,6 +48,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.test.FakeAppErrorStateService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
@ -56,6 +62,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@Suppress("LargeClass")
internal class DefaultInvitePeoplePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@ -605,6 +612,231 @@ internal class DefaultInvitePeoplePresenterTest {
}
}
@Test
fun `present - users are prompted for confirmation if they attempt to invite unknown users`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val inviteUserResult = lambdaRecorder<UserId, Result<Unit>> { userId: UserId ->
Result.success(Unit)
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
inviteUserResult = inviteUserResult,
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItem()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
// If we do not have their identity cached, or fail to fetch it, we should mark them as unknown.
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these users, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(InvitePeopleEvents.SendInvites)
}
delay(1_000)
inviteUserResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - selecting remove on confirmation prompt unselects unknown users`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val repository = FakeUserRepository()
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItemAsDefault()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
// And the search is matching Alice and Bob
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(repository.providedQuery).isEqualTo("some query")
repository.emitState(
UserSearchResultState(
results = listOf(UserSearchResult(alice), UserSearchResult(bob)),
isSearching = true
)
)
skipItems(3)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
// Both Alice and Bob are selected in searchResults
assertThat(
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, true))
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these user, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers)
}
// Selecting "remove" should remove all unknown users, but keeps those who are known.
(awaitLastSequentialItem() as DefaultInvitePeopleState).run {
assertThat(sendInvitesAction.isUninitialized()).isTrue()
assertThat(selectedUsers).containsExactly(alice)
// Bob is no longer selected in searchResults
assertThat(
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, false))
}
}
}
@Test
fun `present - dismissing confirmation prompt does not affect selection`() = runTest {
val alice = aMatrixUser("@alice:example.com")
val bob = aMatrixUser("@bob:example.com")
val charlie = aMatrixUser("@charlie:example.com")
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { userId ->
when (userId.value) {
alice.userId.value -> Result.success(IdentityState.Pinned)
bob.userId.value -> Result.success(null)
else -> Result.failure(AN_EXCEPTION)
}
}
val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult
)
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
)
presenter.test {
val initialState = awaitItem()
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
delay(100)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
listOf(value(alice.userId)),
listOf(value(bob.userId)),
listOf(value(charlie.userId))
)
// When we then try to invite these user, we should prompt for confirmation first.
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal)
}
// Dismissing should not modify the selection at all
(awaitLastSequentialItem() as DefaultInvitePeopleState).run {
assertThat(sendInvitesAction.isUninitialized()).isTrue()
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
}
}
}
private suspend fun FakeUserRepository.emitStateWithUsers(
users: List<MatrixUser>,
isSearching: Boolean = false
@ -646,6 +878,7 @@ fun TestScope.createDefaultInvitePeoplePresenter(
userRepository: UserRepository = FakeUserRepository(),
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
appErrorStateService: AppErrorStateService = FakeAppErrorStateService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
matrixClient: MatrixClient = FakeMatrixClient(),
): DefaultInvitePeoplePresenter {
return DefaultInvitePeoplePresenter(
@ -655,6 +888,7 @@ fun TestScope.createDefaultInvitePeoplePresenter(
coroutineDispatchers = coroutineDispatchers,
sessionCoroutineScope = backgroundScope,
appErrorStateService = appErrorStateService,
featureFlagService = featureFlagService,
matrixClient = matrixClient,
)
}

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="screen_join_room_decline_and_block_button_title">"Адхіліць і заблакіраваць"</string>
<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>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"%1$s があなたを追放しました。"</string>
<string name="screen_join_room_ban_message">"追放されました"</string>
<string name="screen_join_room_ban_reason">"理由: %1$s"</string>
<string name="screen_join_room_cancel_knock_action">"リクエストをキャンセル"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"キャンセルします"</string>
<string name="screen_join_room_cancel_knock_alert_description">"このルームへの参加のリクエストを本当にキャンセルしますか?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"参加のリクエストをキャンセル"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"拒否してブロックする"</string>
<string name="screen_join_room_decline_and_block_alert_message">"本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"招待を拒否してブロック"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒否してブロック"</string>
<string name="screen_join_room_fail_message">"参加に失敗"</string>
<string name="screen_join_room_fail_reason">"制限付きアクセスまたは招待制です。"</string>
<string name="screen_join_room_forget_action">"忘れる"</string>
<string name="screen_join_room_invite_required_message">"参加するには招待が必要です"</string>
<string name="screen_join_room_invited_by">"以下のユーザーからの招待"</string>
<string name="screen_join_room_join_action">"参加"</string>
<string name="screen_join_room_join_restricted_message">"参加するには、招待またはスペースのメンバーである必要があります。"</string>
<string name="screen_join_room_knock_action">"参加をリクエスト"</string>
<string name="screen_join_room_knock_message_characters_count">"文字数制限 %1$d/%2$d 字"</string>
<string name="screen_join_room_knock_message_description">"メッセージ (任意)"</string>
<string name="screen_join_room_knock_sent_description">"リクエストが承認された場合はルームへの招待が届きます。"</string>
<string name="screen_join_room_knock_sent_title">"参加リクエストを送信しました"</string>
<string name="screen_join_room_loading_alert_message">"ルームのプレビューを表示できません。サーバーまたはネットワークの問題の可能性があります。"</string>
<string name="screen_join_room_loading_alert_title">"ルームのプレビューを表示できません"</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

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_cancel_knock_action">"Hủy yêu cầu"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"Có, hủy"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Bạn có chắc chắn muốn hủy yêu cầu tham gia phòng này không?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Hủy yêu cầu tham gia"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Có, từ chối &amp; chặn"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Từ chối lời mời và chặn"</string>
<string name="screen_join_room_decline_and_block_button_title">"Từ chối và chặn"</string>
<string name="screen_join_room_invited_by">"Được mời bởi"</string>
<string name="screen_join_room_join_action">"Tham gia"</string>
<string name="screen_join_room_knock_message_characters_count">"Số ký tự cho phép: %1$d / %2$d"</string>
<string name="screen_join_room_knock_message_description">"Lời nhắn (tùy chọn)"</string>
<string name="screen_join_room_knock_sent_description">"Bạn sẽ nhận được lời mời tham gia phòng nếu yêu cầu của bạn được chấp nhận."</string>
<string name="screen_join_room_knock_sent_title">"Yêu cầu tham gia đã được gửi"</string>
<string name="screen_join_room_loading_alert_message">"Không thể hiển thị bản xem trước của phòng. Có thể do lỗi mạng hoặc máy chủ."</string>
<string name="screen_join_room_loading_alert_title">"Không thể hiển thị bản xem trước của phòng này"</string>
</resources>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_join_room_ban_by_message">"您已被禁止访问%1$s。"</string>
<string name="screen_join_room_ban_message">"你已被禁止访问"</string>
<string name="screen_join_room_ban_by_message">"您已被 %1$s 封禁。"</string>
<string name="screen_join_room_ban_message">"你已被此房间封禁"</string>
<string name="screen_join_room_ban_reason">"理由:%1$s。"</string>
<string name="screen_join_room_cancel_knock_action">"取消请求"</string>
<string name="screen_join_room_cancel_knock_alert_confirmation">"是的,取消"</string>
<string name="screen_join_room_cancel_knock_alert_description">"您确定要取消加入此房间的请求吗?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"取消加入申请"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"是的,拒绝并屏蔽"</string>
<string name="screen_join_room_decline_and_block_alert_message">"您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。"</string>
<string name="screen_join_room_decline_and_block_alert_message">"您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。"</string>
<string name="screen_join_room_decline_and_block_alert_title">"拒绝邀请并屏蔽"</string>
<string name="screen_join_room_decline_and_block_button_title">"拒绝并屏蔽"</string>
<string name="screen_join_room_fail_message">"加入失败"</string>
@ -29,6 +29,6 @@
<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_knock">"想加入聊天室吗?"</string>
<string name="screen_join_room_title_no_preview">"预览不可用"</string>
</resources>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"すべて承認"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"本当にすべての参加リクエストを承認しますか?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"すべてのリクエストを承認"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"すべて承認"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"リクエストの一部を承認できませんでした。もう一度試しますか?"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"リクエストの承認に一部失敗"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"すべてのリクエストを承認中"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"リクエストを承認できませんでした。もう一度試しますか?"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"リクエストの承認に失敗"</string>
<string name="screen_knock_requests_list_accept_loading_title">"リクエストを承認中"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"拒否して追放する"</string>
<string name="screen_knock_requests_list_ban_alert_description">"本当に %1$s を拒否して追放しますか?このユーザーが再度リクエストを送信することはできなくなります。"</string>
<string name="screen_knock_requests_list_ban_alert_title">"拒否してアクセスから追放"</string>
<string name="screen_knock_requests_list_ban_loading_title">"拒否してアクセスから追放中"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"拒否する"</string>
<string name="screen_knock_requests_list_decline_alert_description">"本当に %1$s の参加リクエストを拒否しますか?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"アクセスを拒否"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"拒否と追放"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"このリクエストを拒否できません。もう一度試しますか?"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"リクエストの拒否に失敗"</string>
<string name="screen_knock_requests_list_decline_loading_title">"参加リクエストを拒否中"</string>
<string name="screen_knock_requests_list_empty_state_description">"ルームへの参加リクエストがある場合は、ここに表示されます。"</string>
<string name="screen_knock_requests_list_empty_state_title">"参加リクエストがありません"</string>
<string name="screen_knock_requests_list_initial_loading_title">"参加リクエストを読み込み中"</string>
<string name="screen_knock_requests_list_title">"参加のリクエスト"</string>
<plurals name="screen_room_multiple_knock_requests_title">
<item quantity="other">"%1$s 他 %2$d 人がルーム参加を希望"</item>
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"すべて表示"</string>
<string name="screen_room_single_knock_request_accept_button_title">"承諾"</string>
<string name="screen_room_single_knock_request_title">"%1$s がこのルームの参加を要求しています"</string>
<string name="screen_room_single_knock_request_view_button_title">"表示"</string>
</resources>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Có, chấp nhận tất cả"</string>
<string name="screen_knock_requests_list_accept_all_alert_description">"Bạn có chắc chắn muốn chấp nhận tất cả các yêu cầu tham gia không?"</string>
<string name="screen_knock_requests_list_accept_all_alert_title">"Chấp nhận tất cả các yêu cầu"</string>
<string name="screen_knock_requests_list_accept_all_button_title">"Chấp nhận tất cả"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_description">"Không thể chấp nhận tất cả yêu cầu. Bạn có muốn thử lại không?"</string>
<string name="screen_knock_requests_list_accept_all_failed_alert_title">"Chấp nhận tất cả yêu cầu thất bại"</string>
<string name="screen_knock_requests_list_accept_all_loading_title">"Đang duyệt tất cả yêu cầu tham gia"</string>
<string name="screen_knock_requests_list_accept_failed_alert_description">"Không thể chấp nhận yêu cầu này. Bạn có muốn thử lại không?"</string>
<string name="screen_knock_requests_list_accept_failed_alert_title">"Chấp nhận yêu cầu thất bại"</string>
<string name="screen_knock_requests_list_accept_loading_title">"Đang duyệt yêu cầu tham gia"</string>
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Có, từ chối và cấm"</string>
<string name="screen_knock_requests_list_ban_alert_description">"Bạn có chắc muốn từ chối và cấm %1$s không? Người dùng này sẽ không thể yêu cầu tham gia phòng này nữa"</string>
<string name="screen_knock_requests_list_ban_alert_title">"Từ chối và cấm truy cập"</string>
<string name="screen_knock_requests_list_ban_loading_title">"Đang từ chối và chặn truy cập"</string>
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Có, từ chối"</string>
<string name="screen_knock_requests_list_decline_alert_description">"Bạn có chắc muốn từ chối yêu cầu tham gia phòng của %1$s không?"</string>
<string name="screen_knock_requests_list_decline_alert_title">"Từ chối truy cập"</string>
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Từ chối và chặn"</string>
<string name="screen_knock_requests_list_decline_failed_alert_description">"Không thể từ chối yêu cầu. Bạn có muốn thử lại không?"</string>
<string name="screen_knock_requests_list_decline_failed_alert_title">"Từ chối yêu cầu thất bại"</string>
<string name="screen_knock_requests_list_decline_loading_title">"Đang từ chối yêu cầu tham gia"</string>
<string name="screen_knock_requests_list_empty_state_description">"Khi ai đó xin vào phòng, bạn sẽ thấy yêu cầu ở đây."</string>
<string name="screen_knock_requests_list_empty_state_title">"Không có yêu cầu tham gia nào đang chờ xử lý"</string>
<string name="screen_knock_requests_list_initial_loading_title">"Đang tải các yêu cầu tham gia…"</string>
<string name="screen_room_single_knock_request_accept_button_title">"Đồng ý"</string>
<string name="screen_room_single_knock_request_view_button_title">"Xem"</string>
</resources>

View file

@ -30,6 +30,6 @@
</plurals>
<string name="screen_room_multiple_knock_requests_view_all_button_title">"查看全部"</string>
<string name="screen_room_single_knock_request_accept_button_title">"接受"</string>
<string name="screen_room_single_knock_request_title">"%1$s想加入这个房间"</string>
<string name="screen_room_single_knock_request_title">"%1$s 想加入此房间"</string>
<string name="screen_room_single_knock_request_view_button_title">"查看"</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="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_select_new_owner_action">"所有者を選択"</string>
<string name="leave_room_alert_select_new_owner_subtitle">"あなたがこのルームの唯一の所有者です。退出する前に所有権を他のユーザーへ譲与する必要があります。"</string>
<string name="leave_room_alert_select_new_owner_title">"所有権の譲与"</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">"Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện này không? Cuộc trò chuyện này không công khai và bạn sẽ không thể tham gia lại nếu không được mời."</string>
<string name="leave_room_alert_empty_subtitle">"Bạn có chắc chắn muốn rời khỏi phòng này không? Bạn là người duy nhất ở đây. Nếu bạn rời đi, sẽ không ai có thể tham gia nữa, kể cả bạn."</string>
<string name="leave_room_alert_private_subtitle">"Bạn có chắc chắn muốn rời khỏi phòng này không? Phòng này không công khai và bạn sẽ không thể tham gia lại nếu không có lời mời."</string>
<string name="leave_room_alert_subtitle">"Bạn có chắc chắn muốn rời khỏi phòng không?"</string>
</resources>

View file

@ -1,16 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"Scansiona il codice QR"</string>
<string name="screen_link_new_device_desktop_step1">"Apri %1$s su un laptop o un computer desktop"</string>
<string name="screen_link_new_device_desktop_step3">"Scansiona il codice QR con questo dispositivo"</string>
<string name="screen_link_new_device_desktop_submit">"Pronto per la scansione"</string>
<string name="screen_link_new_device_desktop_title">"Apri %1$s su un computer desktop per ottenere il codice QR"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"I numeri non corrispondono"</string>
<string name="screen_link_new_device_enter_number_notice">"Inserisci il codice a 2 cifre"</string>
<string name="screen_link_new_device_enter_number_subtitle">"Questo verificherà che la connessione con l\'altro dispositivo sia sicura."</string>
<string name="screen_link_new_device_enter_number_title">"Inserisci il numero visualizzato sull\'altro dispositivo"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Il tuo fornitore di account non supporta %1$s."</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s non supportato"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"Il tuo provider di account non supporta l\'accesso a un nuovo dispositivo tramite codice QR."</string>
<string name="screen_link_new_device_error_not_supported_title">"Codice QR non supportato"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"L\'accesso è stato annullato sull\'altro dispositivo."</string>
<string name="screen_link_new_device_error_request_cancelled_title">"Richiesta di accesso annullata"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"L\'accesso è scaduto. Riprova."</string>
<string name="screen_link_new_device_error_request_timeout_title">"L\'accesso non è stato completato in tempo"</string>
<string name="screen_link_new_device_mobile_step1">"Apri %1$s sull\'altro dispositivo"</string>
<string name="screen_link_new_device_mobile_step2">"Seleziona %1$s"</string>
<string name="screen_link_new_device_mobile_step2_action">"“Accedi con codice QR”"</string>
<string name="screen_link_new_device_mobile_step3">"Scansiona il codice QR qui riportato con l\'altro dispositivo"</string>
<string name="screen_link_new_device_mobile_title">"Apri %1$s sull\'altro dispositivo"</string>
<string name="screen_link_new_device_root_desktop_computer">"Computer desktop"</string>
<string name="screen_link_new_device_root_loading_qr_code">"Caricamento codice QR in corso…"</string>
<string name="screen_link_new_device_root_mobile_device">"Dispositivo mobile"</string>
<string name="screen_link_new_device_root_title">"Che tipo di dispositivo desideri collegare?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"Prova di nuovo e assicurati di aver inserito correttamente il codice a 2 cifre. Se i numeri continuano a non corrispondere, contatta il gestore del tuo account."</string>
<string name="screen_link_new_device_wrong_number_title">"I numeri non corrispondono"</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>
@ -21,6 +38,8 @@
<string name="screen_qr_code_login_error_cancelled_title">"Richiesta di accesso annullata"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"L\'accesso è stato rifiutato sull\'altro dispositivo."</string>
<string name="screen_qr_code_login_error_declined_title">"Accesso rifiutato"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Non devi fare altro."</string>
<string name="screen_qr_code_login_error_device_already_signed_in_title">"L\'altro tuo dispositivo è già connesso"</string>
<string name="screen_qr_code_login_error_expired_subtitle">"L\'accesso è scaduto. Riprova."</string>
<string name="screen_qr_code_login_error_expired_title">"L\'accesso non è stato completato in tempo"</string>
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"L\'altro dispositivo non supporta l\'accesso a %s con un codice QR.

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_link_new_device_desktop_scanning_title">"QRコードを読み取り"</string>
<string name="screen_link_new_device_desktop_step1">"%1$s をコンピュータで開いてください"</string>
<string name="screen_link_new_device_desktop_step3">"この端末でQRコードを読み取る"</string>
<string name="screen_link_new_device_desktop_submit">"読み取る"</string>
<string name="screen_link_new_device_desktop_title">"%1$s をコンピュータで開き、QRコードを表示してください"</string>
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"数字が一致しません"</string>
<string name="screen_link_new_device_enter_number_notice">"2桁の数字を入力してください"</string>
<string name="screen_link_new_device_enter_number_subtitle">"他の端末との接続が安全であることを確認します。"</string>
<string name="screen_link_new_device_enter_number_title">"一方の端末で表示される数字を入力してください"</string>
<string name="screen_link_new_device_error_app_not_supported_subtitle">"アカウント提供元が %1$s に対応していません。"</string>
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s に非対応"</string>
<string name="screen_link_new_device_error_not_supported_subtitle">"あなたのアカウント提供元は、QRコードによる追加のサインインに対応していません。"</string>
<string name="screen_link_new_device_error_not_supported_title">"QRコードに非対応"</string>
<string name="screen_link_new_device_error_request_cancelled_subtitle">"もう一方の端末がサインインをキャンセルしました"</string>
<string name="screen_link_new_device_error_request_cancelled_title">"サインインのリクエストがキャンセルされました"</string>
<string name="screen_link_new_device_error_request_timeout_subtitle">"サインインが無効です。もう一度試してください。"</string>
<string name="screen_link_new_device_error_request_timeout_title">"サインインが時間内に完了しませんでした"</string>
<string name="screen_link_new_device_mobile_step1">"%1$s を他の端末で開いてください"</string>
<string name="screen_link_new_device_mobile_step2">"%1$s を選択してください"</string>
<string name="screen_link_new_device_mobile_step2_action">"\"QRコードでサインイン\""</string>
<string name="screen_link_new_device_mobile_step3">"表示されているQRコードを一方の端末で読み取ってください"</string>
<string name="screen_link_new_device_mobile_title">"%1$s を他の端末で開いてください"</string>
<string name="screen_link_new_device_root_desktop_computer">"コンピュータ"</string>
<string name="screen_link_new_device_root_loading_qr_code">"QRコードを読み込み中…"</string>
<string name="screen_link_new_device_root_mobile_device">"モバイル端末"</string>
<string name="screen_link_new_device_root_title">"どのような端末を使用してサインインしますか?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"入力した2桁の数字が正しいことを確認し、再度試してください。問題が継続する場合はアカウント提供元に問い合わせてください。"</string>
<string name="screen_link_new_device_wrong_number_title">"数字が一致しません"</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_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_device_already_signed_in_subtitle">"他には何もする必要はありません。"</string>
<string name="screen_qr_code_login_error_device_already_signed_in_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">"QRコードを使用した %s へのサインインに他の端末が対応していません。
異なる端末で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_invalid_scan_state_description">"もう一方の端末に表示されているQRコードを使用してください"</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"もう一度やり直してください"</string>
<string name="screen_qr_code_login_invalid_scan_state_subtitle">"QRコードが間違っています"</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_unknown_error_description">"予期せぬ問題が発生しました。もう一度試してください。"</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Thử lại"</string>
</resources>

View file

@ -23,7 +23,7 @@
<string name="screen_link_new_device_mobile_step3">"请用另一台设备扫描此处显示的二维码"</string>
<string name="screen_link_new_device_mobile_title">"在另一台设备上打开 %1$s"</string>
<string name="screen_link_new_device_root_desktop_computer">"台式计算机"</string>
<string name="screen_link_new_device_root_loading_qr_code">"正在加载 QR 码…"</string>
<string name="screen_link_new_device_root_loading_qr_code">"正在加载二维码…"</string>
<string name="screen_link_new_device_root_mobile_device">"移动设备"</string>
<string name="screen_link_new_device_root_title">"您想连接哪种类型的设备?"</string>
<string name="screen_link_new_device_wrong_number_subtitle">"请重试,并确保您已正确输入两位验证码。如果验证码仍然不匹配,请联系您的账户提供商。"</string>

View file

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

View file

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

View file

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

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