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:
commit
0ef6b69a79
912 changed files with 17051 additions and 4425 deletions
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 you’ve 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
15
features/ftue/impl/src/main/res/values-ja/translations.xml
Normal file
15
features/ftue/impl/src/main/res/values-ja/translations.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
10
features/ftue/impl/src/main/res/values-vi/translations.xml
Normal file
10
features/ftue/impl/src/main/res/values-vi/translations.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
56
features/home/impl/src/main/res/values-ja/translations.xml
Normal file
56
features/home/impl/src/main/res/values-ja/translations.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
48
features/home/impl/src/main/res/values-vi/translations.xml
Normal file
48
features/home/impl/src/main/res/values-vi/translations.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary(
|
|||
displayType = displayType,
|
||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||
hasRoomCall = false,
|
||||
activeCallIntent = null,
|
||||
isDirect = false,
|
||||
isFavorite = isFavorite,
|
||||
canonicalAlias = null,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
18
features/invite/impl/src/main/res/values-ja/translations.xml
Normal file
18
features/invite/impl/src/main/res/values-ja/translations.xml
Normal 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>
|
||||
17
features/invite/impl/src/main/res/values-vi/translations.xml
Normal file
17
features/invite/impl/src/main/res/values-vi/translations.xml
Normal 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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 don’t have any chats with this contact. Confirm inviting them to this room before continuing."</item>
|
||||
<item quantity="other">"You currently don’t 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue