Merge branch 'develop' into feature/fga/media_viewer_actions
This commit is contained in:
commit
438fc6bb99
408 changed files with 3011 additions and 2886 deletions
32
features/analytics/api/build.gradle.kts
Normal file
32
features/analytics/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.analytics.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
interface AnalyticsEntryPoint : SimpleFeatureEntryPoint
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api
|
||||
|
||||
sealed interface AnalyticsOptInEvents {
|
||||
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api.preferences
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
||||
interface AnalyticsPreferencesPresenter : Presenter<AnalyticsPreferencesState>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api.preferences
|
||||
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
|
||||
data class AnalyticsPreferencesState(
|
||||
val applicationName: String,
|
||||
val isEnabled: Boolean,
|
||||
val eventSink: (AnalyticsOptInEvents) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api.preferences
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<AnalyticsPreferencesState> {
|
||||
override val values: Sequence<AnalyticsPreferencesState>
|
||||
get() = sequenceOf(
|
||||
aAnalyticsPreferencesState().copy(isEnabled = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
||||
applicationName = "ElementX",
|
||||
isEnabled = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.api.preferences
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun AnalyticsPreferencesView(
|
||||
state: AnalyticsPreferencesState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onEnabledChanged(isEnabled: Boolean) {
|
||||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
||||
}
|
||||
|
||||
PreferenceCategory(title = stringResource(id = StringR.string.screen_analytics_settings_share_data)) {
|
||||
val firstPart = stringResource(id = StringR.string.screen_analytics_settings_help_us_improve, state.applicationName)
|
||||
val secondPart = buildAnnotatedStringWithColoredPart(
|
||||
StringR.string.screen_analytics_settings_read_terms,
|
||||
StringR.string.screen_analytics_settings_read_terms_content_link
|
||||
)
|
||||
val title = "$firstPart\n\n$secondPart"
|
||||
|
||||
PreferenceSwitch(
|
||||
title = title,
|
||||
isChecked = state.isEnabled,
|
||||
onCheckedChange = ::onEnabledChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun buildAnnotatedStringWithColoredPart(
|
||||
@StringRes fullTextRes: Int,
|
||||
@StringRes coloredTextRes: Int,
|
||||
color: Color = LinkColor,
|
||||
underline: Boolean = true,
|
||||
) = buildAnnotatedString {
|
||||
val coloredPart = stringResource(coloredTextRes)
|
||||
val fullText = stringResource(fullTextRes, coloredPart)
|
||||
val startIndex = fullText.indexOf(coloredPart)
|
||||
append(fullText)
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = color,
|
||||
textDecoration = if (underline) TextDecoration.Underline else null
|
||||
), start = startIndex, end = startIndex + coloredPart.length
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnalyticsPreferencesViewLightPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: AnalyticsPreferencesState) {
|
||||
AnalyticsPreferencesView(state)
|
||||
}
|
||||
57
features/analytics/impl/build.gradle.kts
Normal file
57
features/analytics/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.analytics.impl"
|
||||
}
|
||||
|
||||
anvil {
|
||||
generateDaggerFactories.set(true)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.features.analytics.api)
|
||||
api(projects.services.analytics.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.mockk)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
class AnalyticsOptInNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AnalyticsOptInPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
AnalyticsOptInView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnalyticsOptInPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<AnalyticsOptInState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): AnalyticsOptInState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
fun handleEvents(event: AnalyticsOptInEvents) {
|
||||
when (event) {
|
||||
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
||||
}
|
||||
localCoroutineScope.launch {
|
||||
analyticsService.setDidAskUserConsent()
|
||||
}
|
||||
}
|
||||
|
||||
return AnalyticsOptInState(
|
||||
applicationName = buildMeta.applicationName,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setIsEnabled(enabled: Boolean) = launch {
|
||||
analyticsService.setUserConsent(enabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
|
||||
data class AnalyticsOptInState(
|
||||
val applicationName: String,
|
||||
val eventSink: (AnalyticsOptInEvents) -> Unit
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import javax.inject.Inject
|
||||
|
||||
open class AnalyticsOptInStateProvider @Inject constructor(
|
||||
) : PreviewParameterProvider<AnalyticsOptInState> {
|
||||
override val values: Sequence<AnalyticsOptInState>
|
||||
get() = sequenceOf(
|
||||
aAnalyticsOptInState(),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAnalyticsOptInState() = AnalyticsOptInState(
|
||||
applicationName = "ElementX",
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.UnderlineSpan
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.designsystem.LinkColor
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@Composable
|
||||
fun AnalyticsOptInView(
|
||||
state: AnalyticsOptInState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LogCompositions(tag = "Analytics", msg = "Root")
|
||||
val eventSink = state.eventSink
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.element_logo_stars),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_analytics_prompt_help_us_improve, state.applicationName),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedStringWithColoredPart(
|
||||
R.string.screen_analytics_prompt_read_terms,
|
||||
R.string.screen_analytics_prompt_read_terms_content_link
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary)
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_analytics_prompt_data_usage).toAnnotatedString(),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary)
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing).toAnnotatedString(),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary)
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_analytics_prompt_settings),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(text = stringResource(id = StringR.string.action_enable))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(text = stringResource(id = StringR.string.action_not_now))
|
||||
}
|
||||
Spacer(Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||
append(this@toAnnotatedString)
|
||||
val spannable = SpannableString(this@toAnnotatedString)
|
||||
spannable.getSpans(0, spannable.length, Any::class.java).forEach { span ->
|
||||
val start = spannable.getSpanStart(span)
|
||||
val end = spannable.getSpanEnd(span)
|
||||
when (span) {
|
||||
is StyleSpan -> when (span.style) {
|
||||
Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
|
||||
Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
|
||||
Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end)
|
||||
}
|
||||
is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
|
||||
is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun buildAnnotatedStringWithColoredPart(
|
||||
@StringRes fullTextRes: Int,
|
||||
@StringRes coloredTextRes: Int,
|
||||
color: Color = LinkColor,
|
||||
underline: Boolean = true,
|
||||
) = buildAnnotatedString {
|
||||
val coloredPart = stringResource(coloredTextRes)
|
||||
val fullText = stringResource(fullTextRes, coloredPart)
|
||||
val startIndex = fullText.indexOf(coloredPart)
|
||||
append(fullText)
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = color,
|
||||
textDecoration = if (underline) TextDecoration.Underline else null
|
||||
), start = startIndex, end = startIndex + coloredPart.length
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnalyticsOptInViewLightPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewLight {
|
||||
ContentToPreview(state)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnalyticsOptInViewDarkPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewDark {
|
||||
ContentToPreview(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: AnalyticsOptInState) {
|
||||
AnalyticsOptInView(state = state)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAnalyticsEntryPoint @Inject constructor() : AnalyticsEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<AnalyticsOptInNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl.preferences
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
|
||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAnalyticsPreferencesPresenter @Inject constructor(
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : AnalyticsPreferencesPresenter {
|
||||
|
||||
@Composable
|
||||
override fun present(): AnalyticsPreferencesState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val isEnabled = analyticsService.getUserConsent()
|
||||
.collectAsState(initial = false)
|
||||
|
||||
fun handleEvents(event: AnalyticsOptInEvents) {
|
||||
when (event) {
|
||||
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
return AnalyticsPreferencesState(
|
||||
applicationName = buildMeta.applicationName,
|
||||
isEnabled = isEnabled.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setIsEnabled(enabled: Boolean) = launch {
|
||||
analyticsService.setUserConsent(enabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="120dp"
|
||||
android:height="94dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="94">
|
||||
<path
|
||||
android:pathData="M60.396,4.958L60.604,4.958A44.521,44.521 0,0 1,105.125 49.479L105.125,49.479A44.521,44.521 0,0 1,60.604 94L60.396,94A44.521,44.521 0,0 1,15.875 49.479L15.875,49.479A44.521,44.521 0,0 1,60.396 4.958z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M53.228,26.676C53.228,24.958 54.623,23.566 56.344,23.566C67.82,23.566 77.123,32.847 77.123,44.296C77.123,46.014 75.727,47.406 74.006,47.406C72.285,47.406 70.889,46.014 70.889,44.296C70.889,36.282 64.377,29.785 56.344,29.785C54.623,29.785 53.228,28.393 53.228,26.676Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M67.772,72.282C67.772,73.999 66.377,75.391 64.655,75.391C53.18,75.391 43.877,66.11 43.877,54.661C43.877,52.944 45.272,51.552 46.994,51.552C48.715,51.552 50.111,52.944 50.111,54.661C50.111,62.675 56.623,69.172 64.655,69.172C66.377,69.172 67.772,70.564 67.772,72.282Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M37.644,56.734C35.922,56.734 34.527,55.342 34.527,53.625C34.527,42.176 43.83,32.895 55.305,32.895C57.027,32.895 58.422,34.287 58.422,36.004C58.422,37.722 57.027,39.114 55.305,39.114C47.272,39.114 40.76,45.611 40.76,53.625C40.76,55.342 39.365,56.734 37.644,56.734Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.356,42.223C85.078,42.223 86.473,43.615 86.473,45.332C86.473,56.781 77.17,66.063 65.695,66.063C63.973,66.063 62.578,64.671 62.578,62.953C62.578,61.236 63.973,59.844 65.695,59.844C73.728,59.844 80.24,53.347 80.24,45.332C80.24,43.615 81.635,42.223 83.356,42.223Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M39.571,77.305C40.181,77.305 40.683,76.856 40.769,76.227C41.7,69.687 42.587,68.79 48.918,68.076C49.56,68.001 50.041,67.478 50.041,66.87C50.041,66.251 49.57,65.75 48.929,65.664C42.63,64.843 41.817,64.043 40.769,57.502C40.662,56.873 40.181,56.435 39.571,56.435C38.972,56.435 38.47,56.873 38.374,57.513C37.454,64.053 36.566,64.95 30.235,65.664C29.594,65.739 29.123,66.251 29.123,66.87C29.123,67.478 29.583,67.99 30.235,68.076C36.534,68.951 37.315,69.697 38.374,76.238C38.491,76.867 38.983,77.305 39.571,77.305Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#0DBD8B"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M82.194,35.392C82.697,35.392 83.111,35.019 83.182,34.494C83.949,29.044 84.682,28.297 89.905,27.701C90.434,27.639 90.831,27.203 90.831,26.697C90.831,26.181 90.443,25.763 89.913,25.692C84.717,25.007 84.046,24.34 83.182,18.89C83.093,18.365 82.697,18.001 82.194,18.001C81.7,18.001 81.285,18.365 81.205,18.899C80.447,24.349 79.714,25.096 74.491,25.692C73.962,25.754 73.574,26.181 73.574,26.697C73.574,27.203 73.953,27.63 74.491,27.701C79.688,28.43 80.332,29.053 81.205,34.503C81.302,35.028 81.708,35.392 82.194,35.392Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M113.846,18.87C114.174,18.87 114.444,18.631 114.49,18.296C114.991,14.807 115.468,14.329 118.873,13.948C119.218,13.908 119.477,13.63 119.477,13.305C119.477,12.975 119.224,12.708 118.879,12.662C115.491,12.224 115.054,11.797 114.49,8.309C114.433,7.973 114.174,7.74 113.846,7.74C113.524,7.74 113.254,7.973 113.202,8.315C112.707,11.803 112.23,12.281 108.825,12.662C108.48,12.702 108.227,12.975 108.227,13.305C108.227,13.63 108.474,13.903 108.825,13.948C112.213,14.415 112.633,14.813 113.202,18.301C113.265,18.637 113.53,18.87 113.846,18.87Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M107.169,9.131C107.354,9.131 107.506,8.997 107.531,8.808C107.813,6.846 108.081,6.577 109.997,6.363C110.191,6.34 110.336,6.183 110.336,6.001C110.336,5.815 110.194,5.665 110,5.639C108.094,5.393 107.849,5.153 107.531,3.191C107.499,3.002 107.354,2.871 107.169,2.871C106.988,2.871 106.836,3.002 106.807,3.194C106.529,5.156 106.26,5.425 104.345,5.639C104.151,5.662 104.008,5.815 104.008,6.001C104.008,6.183 104.147,6.337 104.345,6.363C106.25,6.625 106.486,6.849 106.807,8.811C106.842,9 106.991,9.131 107.169,9.131Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M108.575,24.435C108.8,24.435 108.986,24.271 109.018,24.04C109.362,21.642 109.69,21.314 112.031,21.052C112.268,21.024 112.446,20.833 112.446,20.61C112.446,20.383 112.272,20.199 112.035,20.167C109.706,19.866 109.405,19.573 109.018,17.175C108.978,16.944 108.8,16.783 108.575,16.783C108.353,16.783 108.167,16.944 108.132,17.179C107.792,19.577 107.464,19.905 105.123,20.167C104.885,20.195 104.711,20.383 104.711,20.61C104.711,20.833 104.881,21.02 105.123,21.052C107.452,21.372 107.74,21.646 108.132,24.044C108.175,24.275 108.357,24.435 108.575,24.435Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M6.197,15.392C6.504,15.392 6.758,15.168 6.801,14.853C7.27,11.583 7.718,11.135 10.91,10.778C11.233,10.74 11.476,10.479 11.476,10.175C11.476,9.865 11.239,9.615 10.915,9.572C7.739,9.161 7.329,8.761 6.801,5.491C6.747,5.176 6.504,4.958 6.197,4.958C5.895,4.958 5.642,5.176 5.593,5.496C5.129,8.767 4.682,9.215 1.49,9.572C1.166,9.609 0.929,9.865 0.929,10.175C0.929,10.479 1.161,10.735 1.49,10.778C4.666,11.215 5.059,11.589 5.593,14.859C5.652,15.174 5.9,15.392 6.197,15.392Z"
|
||||
android:fillColor="#0DBD8B"/>
|
||||
<path
|
||||
android:pathData="M13.231,5.653C13.375,5.653 13.493,5.549 13.513,5.402C13.732,3.876 13.941,3.667 15.431,3.5C15.582,3.482 15.695,3.36 15.695,3.218C15.695,3.074 15.584,2.957 15.433,2.937C13.951,2.745 13.76,2.559 13.513,1.033C13.488,0.886 13.375,0.784 13.231,0.784C13.09,0.784 12.972,0.886 12.95,1.035C12.733,2.561 12.524,2.77 11.035,2.937C10.884,2.955 10.773,3.074 10.773,3.218C10.773,3.36 10.881,3.48 11.035,3.5C12.517,3.704 12.7,3.878 12.95,5.404C12.977,5.551 13.093,5.653 13.231,5.653Z"
|
||||
android:strokeAlpha="0.4"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:fillAlpha="0.4"/>
|
||||
<path
|
||||
android:pathData="M16.747,11.914C16.89,11.914 17.009,11.809 17.029,11.663C17.248,10.136 17.457,9.927 18.946,9.761C19.097,9.743 19.21,9.621 19.21,9.479C19.21,9.335 19.1,9.218 18.949,9.198C17.467,9.006 17.275,8.819 17.029,7.293C17.004,7.147 16.89,7.044 16.747,7.044C16.606,7.044 16.488,7.147 16.465,7.296C16.249,8.822 16.04,9.031 14.55,9.198C14.399,9.215 14.289,9.335 14.289,9.479C14.289,9.621 14.397,9.741 14.55,9.761C16.032,9.965 16.216,10.139 16.465,11.665C16.493,11.812 16.609,11.914 16.747,11.914Z"
|
||||
android:strokeAlpha="0.4"
|
||||
android:fillColor="#0DBD8B"
|
||||
android:fillAlpha="0.4"/>
|
||||
</vector>
|
||||
|
|
@ -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">"Wir erfassen und analysieren "<b>"keine"</b>" Account-Daten"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Sie können alle unsere Nutzerbedingungen %1$s lesen."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"hier"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Sie können die Analyse jederzeit in den Einstellungen deaktivieren"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Wir geben "<b>"keine"</b>" Informationen an Dritte weiter"</string>
|
||||
<string name="screen_analytics_prompt_title">"Helfen Sie %1$s zu verbessern"</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"><b>"Nu"</b>" înregistrăm sau profilăm datele contului"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"aici"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Puteți dezactiva această opțiune oricând din setări"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing"><b>"Nu"</b>" împărtășim informații cu terți"</string>
|
||||
<string name="screen_analytics_prompt_title">"Ajutați la îmbunătățirea %1$s"</string>
|
||||
</resources>
|
||||
10
features/analytics/impl/src/main/res/values/localazy.xml
Normal file
10
features/analytics/impl/src/main/res/values/localazy.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_analytics_prompt_data_usage">"We "<b>"don\'t"</b>" record or profile any account data"</string>
|
||||
<string name="screen_analytics_prompt_help_us_improve">"Help us identify issues and improve %1$s by sharing anonymous usage data."</string>
|
||||
<string name="screen_analytics_prompt_read_terms">"You can read all our terms %1$s."</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"here"</string>
|
||||
<string name="screen_analytics_prompt_settings">"You can turn this off anytime in settings"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"We "<b>"don\'t"</b>" share information with third parties"</string>
|
||||
<string name="screen_analytics_prompt_title">"Help improve %1$s"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter
|
||||
import io.element.android.features.analytics.test.A_BUILD_META
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AnalyticsOptInPresenterTest {
|
||||
@Test
|
||||
fun `present - enable`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService(isEnabled = false)
|
||||
val presenter = AnalyticsOptInPresenter(
|
||||
A_BUILD_META,
|
||||
analyticsService
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(analyticsService.didAskUserConsent().first()).isFalse()
|
||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
||||
assertThat(analyticsService.didAskUserConsent().first()).isTrue()
|
||||
assertThat(analyticsService.getUserConsent().first()).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - not now`() = runTest {
|
||||
val analyticsService = FakeAnalyticsService(isEnabled = false)
|
||||
val presenter = AnalyticsOptInPresenter(
|
||||
A_BUILD_META,
|
||||
analyticsService
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(analyticsService.didAskUserConsent().first()).isFalse()
|
||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
||||
assertThat(analyticsService.didAskUserConsent().first()).isTrue()
|
||||
assertThat(analyticsService.getUserConsent().first()).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.impl.preferences
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.features.analytics.test.A_BUILD_META
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AnalyticsPreferencesPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state available`() = runTest {
|
||||
val presenter = DefaultAnalyticsPreferencesPresenter(
|
||||
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
||||
A_BUILD_META
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state not available`() = runTest {
|
||||
val presenter = DefaultAnalyticsPreferencesPresenter(
|
||||
FakeAnalyticsService(isEnabled = false, didAskUserConsent = false),
|
||||
A_BUILD_META
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - enable and disable`() = runTest {
|
||||
val presenter = DefaultAnalyticsPreferencesPresenter(
|
||||
FakeAnalyticsService(isEnabled = true, didAskUserConsent = true),
|
||||
A_BUILD_META
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isEnabled).isTrue()
|
||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
||||
assertThat(awaitItem().isEnabled).isFalse()
|
||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
||||
assertThat(awaitItem().isEnabled).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
features/analytics/test/build.gradle.kts
Normal file
28
features/analytics/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.analytics.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.test
|
||||
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeAnalyticsService(
|
||||
isEnabled: Boolean = false,
|
||||
didAskUserConsent: Boolean = false
|
||||
): AnalyticsService {
|
||||
|
||||
private var isEnabledFlow = MutableStateFlow(isEnabled)
|
||||
private var didAskUserConsentFlow = MutableStateFlow(didAskUserConsent)
|
||||
|
||||
override fun getAvailableAnalyticsProviders(): List<AnalyticsProvider> = emptyList()
|
||||
|
||||
override fun getUserConsent(): Flow<Boolean> = isEnabledFlow
|
||||
|
||||
override suspend fun setUserConsent(userConsent: Boolean) {
|
||||
isEnabledFlow.value = userConsent
|
||||
}
|
||||
|
||||
override fun didAskUserConsent(): Flow<Boolean> = didAskUserConsentFlow
|
||||
|
||||
override suspend fun setDidAskUserConsent() {
|
||||
didAskUserConsentFlow.value = true
|
||||
}
|
||||
|
||||
override fun getAnalyticsId(): Flow<String> = MutableStateFlow("")
|
||||
|
||||
override suspend fun setAnalyticsId(analyticsId: String) {
|
||||
}
|
||||
|
||||
override suspend fun onSignOut() {
|
||||
}
|
||||
|
||||
override fun capture(event: VectorAnalyticsEvent) {
|
||||
}
|
||||
|
||||
override fun screen(screen: VectorAnalyticsScreen) {
|
||||
}
|
||||
|
||||
override fun updateUserProperties(userProperties: UserProperties) {
|
||||
}
|
||||
|
||||
override fun trackError(throwable: Throwable) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.analytics.test
|
||||
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
|
||||
val A_BUILD_META = BuildMeta(
|
||||
isDebuggable = true,
|
||||
buildType = BuildType.DEBUG,
|
||||
applicationName = "Element X test",
|
||||
applicationId = "",
|
||||
lowPrivacyLoggingEnabled = false,
|
||||
versionName = "",
|
||||
gitRevision = "",
|
||||
gitRevisionDate = "",
|
||||
gitBranchName = "",
|
||||
flavorDescription = "",
|
||||
flavorShortDescription = "",
|
||||
)
|
||||
|
||||
|
|
@ -16,15 +16,26 @@
|
|||
|
||||
package io.element.android.features.createroom.impl.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -49,6 +60,8 @@ fun SearchUserBar(
|
|||
onUserSelected: (MatrixUser) -> Unit = {},
|
||||
onUserDeselected: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
val columnState = rememberLazyListState()
|
||||
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onTextChanged,
|
||||
|
|
@ -59,19 +72,38 @@ fun SearchUserBar(
|
|||
showBackButton = showBackButton,
|
||||
contentPrefix = {
|
||||
if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) {
|
||||
// We want the selected users to behave a bit like a top bar - when the list below is scrolled, the colour
|
||||
// should change to indicate elevation.
|
||||
|
||||
val elevation = remember {
|
||||
derivedStateOf {
|
||||
if (columnState.canScrollBackward) {
|
||||
4.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val appBarContainerColor by animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation.value),
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||
)
|
||||
|
||||
SelectedUsersList(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
selectedUsers = selectedUsers,
|
||||
autoScroll = true,
|
||||
onUserRemoved = onUserDeselected,
|
||||
modifier = Modifier.background(appBarContainerColor)
|
||||
)
|
||||
}
|
||||
},
|
||||
resultState = state,
|
||||
resultHandler = { users ->
|
||||
LazyColumn {
|
||||
LazyColumn(state = columnState) {
|
||||
if (isMultiSelectionEnabled) {
|
||||
items(users) { searchResult ->
|
||||
itemsIndexed(users) { index, searchResult ->
|
||||
SearchMultipleUsersResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
searchResult = searchResult,
|
||||
|
|
@ -84,14 +116,20 @@ fun SearchUserBar(
|
|||
}
|
||||
}
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(users) { searchResult ->
|
||||
itemsIndexed(users) { index, searchResult ->
|
||||
SearchSingleUserResultItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
searchResult = searchResult,
|
||||
onClick = { onUserSelected(searchResult.matrixUser) }
|
||||
)
|
||||
if (index < users.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ fun ConfigureRoomView(
|
|||
)
|
||||
if (state.config.invites.isNotEmpty()) {
|
||||
SelectedUsersList(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp),
|
||||
selectedUsers = state.config.invites,
|
||||
onUserRemoved = {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,15 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -142,7 +143,11 @@ fun CreateRoomRootViewTopBar(
|
|||
},
|
||||
actions = {
|
||||
IconButton(onClick = onClosePressed) {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = stringResource(id = StringR.string.action_close))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(id = StringR.string.action_close),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -157,7 +162,7 @@ fun CreateRoomActionButtonsList(
|
|||
Column(modifier = modifier) {
|
||||
CreateRoomActionButton(
|
||||
iconRes = DrawableR.drawable.ic_groups,
|
||||
text = stringResource(id = StringR.string.action_create_a_room),
|
||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
||||
onClick = onNewRoomClicked,
|
||||
)
|
||||
CreateRoomActionButton(
|
||||
|
|
@ -185,11 +190,16 @@ fun CreateRoomActionButton(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.alpha(0.5f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
resourceId = iconRes,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(text = text)
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
<?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">"Nová místnost"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Pozvat lidi"</string>
|
||||
<string name="screen_create_room_add_people_title">"Přidat lidi"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Pozvat přátele do Elementu"</string>
|
||||
<string name="screen_create_room_add_people_title">"Pozvat lidi"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Při vytváření místnosti došlo k chybě"</string>
|
||||
<string name="screen_create_room_private_option_description">"Zprávy v této místnosti jsou šifrované. Šifrování nelze později vypnout."</string>
|
||||
<string name="screen_create_room_private_option_title">"Soukromá místnost (jen pro pozvané)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později."</string>
|
||||
<string name="screen_create_room_public_option_title">"Veřejná místnost (kdokoli)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"např. Produktový sprint"</string>
|
||||
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"O čem je tato místnost?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
|
||||
<string name="screen_create_room_title">"Vytvořit místnost"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
<?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">"Neuer Raum"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Personen einladen"</string>
|
||||
<string name="screen_create_room_add_people_title">"Personen hinzufügen"</string>
|
||||
<string name="screen_create_room_action_invite_people">"Freunde zu Element einladen"</string>
|
||||
<string name="screen_create_room_add_people_title">"Personen einladen"</string>
|
||||
<string name="screen_create_room_error_creating_room">"Beim Erstellen des Raums ist ein Fehler aufgetreten"</string>
|
||||
<string name="screen_create_room_private_option_description">"Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."</string>
|
||||
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
|
||||
<string name="screen_create_room_public_option_description">"Nachrichten sind nicht verschlüsselt und jeder kann sie lesen. Du kannst die Verschlüsselung zu einem späteren Zeitpunkt aktivieren."</string>
|
||||
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (jeder)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Raumname"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"z.B. Produkt-Sprint"</string>
|
||||
<string name="screen_create_room_topic_label">"Thema (optional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Worum geht es in diesem Raum?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
|
||||
<string name="screen_create_room_title">"Raum erstellen"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
||||
<string name="screen_create_room_title">"Crear una sala"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
@ -9,9 +9,7 @@
|
|||
<string name="screen_create_room_public_option_description">"Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."</string>
|
||||
<string name="screen_create_room_public_option_title">"Salle publique (n’importe qui)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Nom de la salle"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"Ex: Sprint Produit"</string>
|
||||
<string name="screen_create_room_topic_label">"Sujet (optionnel)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"De quoi parle cette salle ?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Une erreur s\'est produite lors de la tentative de démarrage d\'une discussion"</string>
|
||||
<string name="screen_create_room_title">"Créer une salle"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
||||
<string name="screen_create_room_title">"Crea una stanza"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
@ -9,9 +9,7 @@
|
|||
<string name="screen_create_room_public_option_description">"Mesajele nu sunt criptate și oricine le poate citi. Puteți activa criptarea la o dată ulterioară."</string>
|
||||
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
|
||||
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
|
||||
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
|
||||
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
|
||||
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
|
||||
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||
<string name="screen_create_room_title">"Creați o cameră"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation
|
|||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
|
|
@ -81,6 +82,9 @@ fun InviteListView(
|
|||
ConfirmationDialog(
|
||||
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
||||
title = stringResource(titleResource),
|
||||
submitText = stringResource(StringR.string.action_decline),
|
||||
cancelText = stringResource(StringR.string.action_cancel),
|
||||
emphasizeSubmitButton = true,
|
||||
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
||||
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
||||
)
|
||||
|
|
@ -143,14 +147,18 @@ fun InviteListContent(
|
|||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.inviteList,
|
||||
) { invite ->
|
||||
) { index, invite ->
|
||||
InviteSummaryRow(
|
||||
invite = invite,
|
||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||
)
|
||||
|
||||
if (index != state.inviteList.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@
|
|||
package io.element.android.features.invitelist.impl.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
|
|
@ -31,23 +31,18 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -64,8 +59,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.noFontPadding
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
private val minHeight = 72.dp
|
||||
|
|
@ -104,23 +99,27 @@ internal fun DefaultInviteSummaryRow(
|
|||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Avatar(
|
||||
invite.roomAvatarData,
|
||||
invite.roomAvatarData.copy(size = AvatarSize.Custom(52.dp)),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp)
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
) {
|
||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
||||
|
||||
// Name
|
||||
Text(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontWeight = FontWeight.Medium,
|
||||
text = invite.roomName,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = noFontPadding,
|
||||
modifier = Modifier.padding(end = bonusPadding),
|
||||
)
|
||||
|
||||
// ID or Alias
|
||||
|
|
@ -131,7 +130,8 @@ internal fun DefaultInviteSummaryRow(
|
|||
text = it,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(end = bonusPadding),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -145,8 +145,8 @@ internal fun DefaultInviteSummaryRow(
|
|||
OutlinedButton(
|
||||
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
|
||||
onClick = onDeclineClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
|
||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
|
@ -154,8 +154,8 @@ internal fun DefaultInviteSummaryRow(
|
|||
Button(
|
||||
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
|
||||
onClick = onAcceptClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
|
||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,45 +173,36 @@ internal fun DefaultInviteSummaryRow(
|
|||
|
||||
@Composable
|
||||
private fun SenderRow(sender: InviteSender) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
val placeholder = "$"
|
||||
val text = stringResource(R.string.screen_invites_invited_you, placeholder)
|
||||
val nameIndex = text.indexOf(placeholder)
|
||||
|
||||
// Text before the placeholder
|
||||
append(text.take(nameIndex))
|
||||
|
||||
// Avatar and display name
|
||||
appendInlineContent("avatar")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)) {
|
||||
append(sender.displayName)
|
||||
}
|
||||
|
||||
// Text after the placeholder
|
||||
append(text.drop(nameIndex + placeholder.length))
|
||||
},
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
inlineContent = persistentMapOf(
|
||||
"avatar" to InlineTextContent(
|
||||
with(LocalDensity.current) {
|
||||
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
||||
AnnotatedString(
|
||||
text = text,
|
||||
spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
start = senderNameStart,
|
||||
end = senderNameStart + sender.displayName.length
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
style = noFontPadding,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
|
|||
override val values: Sequence<InviteListInviteSummary>
|
||||
get() = sequenceOf(
|
||||
aInviteListInviteSummary(),
|
||||
aInviteListInviteSummary().copy(roomAlias = "#someroom:example.com"),
|
||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com"),
|
||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com", isNew = true),
|
||||
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
||||
aInviteListInviteSummary().copy(isNew = true)
|
||||
)
|
||||
|
|
@ -32,9 +33,9 @@ open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteList
|
|||
|
||||
fun aInviteListInviteSummary() = InviteListInviteSummary(
|
||||
roomId = RoomId("!room1:example.com"),
|
||||
roomName = "Some room",
|
||||
roomName = "Some room with a long name that will truncate",
|
||||
sender = InviteSender(
|
||||
userId = UserId("@alice:example.org"),
|
||||
displayName = "Alice"
|
||||
userId = UserId("@alice-with-a-long-mxid:example.org"),
|
||||
displayName = "Alice with a long name"
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@
|
|||
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout chat s %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Odmítnout chat"</string>
|
||||
<string name="screen_invites_empty_list">"Žádné pozvánky"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval(a)"</string>
|
||||
</resources>
|
||||
|
|
@ -5,5 +5,4 @@
|
|||
<string name="screen_invites_decline_direct_chat_message">"Möchten Sie den Chat mit %1$s wirklich ablehnen?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Chat ablehnen"</string>
|
||||
<string name="screen_invites_empty_list">"Keine Einladungen"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s hat dich eingeladen"</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s te invitó."</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_empty_list">"Aucune invitation"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s vous a invité."</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s ti ha invitato"</string>
|
||||
</resources>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ti ha invitato"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Refuzați conversația"</string>
|
||||
<string name="screen_invites_empty_list">"Nicio invitație"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s v-a invitat"</string>
|
||||
</resources>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) v-a invitat."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -27,15 +27,10 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
|
|||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.Generic
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.LastUserInRoom
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState.Confirmation.PrivateRoom
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -83,7 +78,7 @@ class LeaveRoomPresenterImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun showLeaveRoomAlert(
|
||||
private fun showLeaveRoomAlert(
|
||||
matrixClient: MatrixClient,
|
||||
roomId: RoomId,
|
||||
confirmation: MutableState<LeaveRoomState.Confirmation>,
|
||||
|
|
@ -91,7 +86,7 @@ private suspend fun showLeaveRoomAlert(
|
|||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
confirmation.value = when {
|
||||
!room.isPublic -> PrivateRoom(roomId)
|
||||
(room.memberCount() as? Async.Success<Int>)?.state == 1 -> LastUserInRoom(roomId)
|
||||
room.joinedMemberCount == 1L -> LastUserInRoom(roomId)
|
||||
else -> Generic(roomId)
|
||||
}
|
||||
}
|
||||
|
|
@ -116,12 +111,3 @@ private suspend fun MatrixClient.leaveRoom(
|
|||
}
|
||||
progress.value = LeaveRoomState.Progress.Hidden
|
||||
}
|
||||
|
||||
private suspend fun MatrixRoom.memberCount(): Async<Int> = membersStateFlow.first().let { membersState ->
|
||||
when (membersState) {
|
||||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
|||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -100,24 +96,7 @@ class LeaveRoomPresenterImplTest {
|
|||
client = FakeMatrixClient().apply {
|
||||
givenGetRoomResult(
|
||||
roomId = A_ROOM_ID,
|
||||
result = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
listOf(
|
||||
RoomMember(
|
||||
userId = UserId(value = "@aUserId:aDomain"),
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
result = FakeMatrixRoom(joinedMemberCount = 1),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.features.login.impl.oidc.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceRequest
|
||||
|
|
@ -26,8 +25,6 @@ import android.webkit.WebViewClient
|
|||
class OidcWebViewClient(
|
||||
private val eventListener: WebViewEventListener,
|
||||
) : WebViewClient() {
|
||||
// We will revert to API 23, in the mean time ignore the warning here.
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
return shouldOverrideUrl(request.url.toString())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc."</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"Tento server v současné době nepodporuje klouzavou synchronizaci."</string>
|
||||
<string name="screen_change_server_form_header">"Adresa URL domovského serveru"</string>
|
||||
<string name="screen_change_server_form_notice">"Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Jaká je adresa vašeho serveru?"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Tento účet byl deaktivován."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Nesprávné uživatelské jméno nebo heslo"</string>
|
||||
|
|
@ -10,6 +12,7 @@
|
|||
<string name="screen_login_form_header">"Zadejte své údaje"</string>
|
||||
<string name="screen_login_server_header">"Kde budou vaše konverzace probíhat"</string>
|
||||
<string name="screen_login_title">"Vítejte zpět!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Přihlaste se k %1$s"</string>
|
||||
<string name="screen_change_server_submit">"Pokračovat"</string>
|
||||
<string name="screen_change_server_title">"Vyberte svůj server"</string>
|
||||
<string name="screen_login_password_hint">"Heslo"</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<string name="screen_login_form_header">"Gib deine Daten ein"</string>
|
||||
<string name="screen_login_server_header">"Wo deine Gespräche leben"</string>
|
||||
<string name="screen_login_title">"Willkommen zurück!"</string>
|
||||
<string name="screen_login_title_with_homeserver">"Bei %1$s anmelden"</string>
|
||||
<string name="screen_change_server_submit">"Weiter"</string>
|
||||
<string name="screen_change_server_title">"Wählen deinen Server"</string>
|
||||
<string name="screen_login_password_hint">"Passwort"</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d změna místnosti"</item>
|
||||
<item quantity="few">"%1$d změny místnosti"</item>
|
||||
<item quantity="other">"%1$d změn místnosti"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Fotoaparát"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Vyfotit"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Natočit video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Příloha"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
</resources>
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"Record a video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Attachment"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Photo & Video Library"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_sign_in_manually">"Ruční přihlášení"</string>
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Přihlásit se pomocí QR kódu"</string>
|
||||
<string name="screen_onboarding_sign_up">"Vytvořit účet"</string>
|
||||
<string name="screen_onboarding_subtitle">"Komunikujte a spolupracujte bezpečně"</string>
|
||||
<string name="screen_onboarding_welcome_subtitle">"Vítejte v %1$s Beta. Vylepšený, pro rychlost a jednoduchost."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Buďte ve svém živlu"</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_onboarding_sign_in_with_qr_code">"Mit QR-Code anmelden"</string>
|
||||
<string name="screen_onboarding_sign_up">"Konto erstellen"</string>
|
||||
<string name="screen_onboarding_welcome_subtitle">"Willkommen zur %1$s Beta. Verbessert, für Geschwindigkeit und Einfachheit."</string>
|
||||
<string name="screen_onboarding_welcome_title">"Sei in deinem Element"</string>
|
||||
</resources>
|
||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(libs.datetime)
|
||||
|
|
@ -59,6 +60,8 @@ dependencies {
|
|||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.features.rageshake.impl)
|
||||
testImplementation(projects.features.logout.impl)
|
||||
testImplementation(projects.features.analytics.test)
|
||||
testImplementation(projects.features.analytics.impl)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter
|
||||
import io.element.android.features.logout.api.LogoutPreferencePresenter
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -27,6 +28,7 @@ import javax.inject.Inject
|
|||
class PreferencesRootPresenter @Inject constructor(
|
||||
private val logoutPresenter: LogoutPreferencePresenter,
|
||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||
private val analyticsPresenter: AnalyticsPreferencesPresenter,
|
||||
private val buildType: BuildType,
|
||||
) : Presenter<PreferencesRootState> {
|
||||
|
||||
|
|
@ -34,10 +36,12 @@ class PreferencesRootPresenter @Inject constructor(
|
|||
override fun present(): PreferencesRootState {
|
||||
val logoutState = logoutPresenter.present()
|
||||
val rageshakeState = rageshakePresenter.present()
|
||||
val analyticsState = analyticsPresenter.present()
|
||||
val showDeveloperSettings = buildType != BuildType.RELEASE
|
||||
return PreferencesRootState(
|
||||
logoutState = logoutState,
|
||||
rageshakeState = rageshakeState,
|
||||
analyticsState = analyticsState,
|
||||
myUser = Async.Uninitialized,
|
||||
showDeveloperSettings = showDeveloperSettings
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
||||
import io.element.android.features.logout.api.LogoutPreferenceState
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -24,6 +25,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
|
|||
data class PreferencesRootState(
|
||||
val logoutState: LogoutPreferenceState,
|
||||
val rageshakeState: RageshakePreferencesState,
|
||||
val analyticsState: AnalyticsPreferencesState,
|
||||
val myUser: Async<MatrixUser>,
|
||||
val showDeveloperSettings: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState
|
||||
import io.element.android.features.logout.api.aLogoutPreferenceState
|
||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
|
|
@ -23,6 +24,7 @@ import io.element.android.libraries.architecture.Async
|
|||
fun aPreferencesRootState() = PreferencesRootState(
|
||||
logoutState = aLogoutPreferenceState(),
|
||||
rageshakeState = aRageshakePreferencesState(),
|
||||
analyticsState = aAnalyticsPreferencesState(),
|
||||
myUser = Async.Uninitialized,
|
||||
showDeveloperSettings = true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.logout.api.LogoutPreferenceView
|
||||
import io.element.android.features.preferences.impl.user.UserPreferences
|
||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesView
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
|
|
@ -52,6 +53,9 @@ fun PreferencesRootView(
|
|||
title = stringResource(id = StringR.string.common_settings)
|
||||
) {
|
||||
UserPreferences(state.myUser)
|
||||
AnalyticsPreferencesView(
|
||||
state = state.analyticsState,
|
||||
)
|
||||
RageshakePreferencesView(
|
||||
state = state.rageshakeState,
|
||||
onOpenRageshake = onOpenRageShake,
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.impl.preferences.DefaultAnalyticsPreferencesPresenter
|
||||
import io.element.android.features.analytics.test.A_BUILD_META
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter
|
||||
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -35,10 +37,12 @@ class PreferencesRootPresenterTest {
|
|||
fun `present - initial state`() = runTest {
|
||||
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient())
|
||||
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
|
||||
val analyticsPresenter = DefaultAnalyticsPreferencesPresenter(FakeAnalyticsService(), A_BUILD_META)
|
||||
val presenter = PreferencesRootPresenter(
|
||||
logoutPresenter,
|
||||
rageshakePresenter,
|
||||
BuildType.DEBUG
|
||||
analyticsPresenter,
|
||||
A_BUILD_META.buildType
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -46,6 +50,7 @@ class PreferencesRootPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.analyticsState.isEnabled).isFalse()
|
||||
assertThat(initialState.rageshakeState.isEnabled).isTrue()
|
||||
assertThat(initialState.rageshakeState.isSupported).isTrue()
|
||||
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="crash_detection_dialog_content">"%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?"</string>
|
||||
</resources>
|
||||
|
|
@ -1,4 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_bug_report_attach_screenshot">"Připojit snímek obrazovky"</string>
|
||||
<string name="screen_bug_report_contact_me">"V případě dalších dotazů se na mě můžete obrátit"</string>
|
||||
<string name="screen_bug_report_edit_screenshot">"Upravit snímek obrazovky"</string>
|
||||
<string name="screen_bug_report_editor_description">"Popište prosím chybu. Co jste udělali? Co jste očekávali, že se stane? Co se ve skutečnosti stalo? Uveďte co nejvíce podrobností."</string>
|
||||
<string name="screen_bug_report_editor_placeholder">"Popište chybu…"</string>
|
||||
<string name="screen_bug_report_editor_supporting">"Pokud je to možné, prosím, napište popis anglicky."</string>
|
||||
<string name="screen_bug_report_include_crash_logs">"Odeslat záznamy o selhání"</string>
|
||||
<string name="screen_bug_report_include_logs">"Odeslat protokoly pro pomoc"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Odeslat snímek obrazovky"</string>
|
||||
<string name="screen_bug_report_logs_description">"Aby bylo možné zkontrolovat, zda věci fungují podle očekávání, budou s vaší zprávou odeslány protokoly. Tyto budou soukromé. Chcete-li pouze odeslat zprávu, vypněte toto nastavení."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"</string>
|
||||
</resources>
|
||||
|
|
@ -27,12 +27,10 @@ import androidx.compose.runtime.remember
|
|||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import javax.inject.Inject
|
||||
|
|
@ -51,7 +49,6 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
val membersState by room.membersStateFlow.collectAsState()
|
||||
val memberCount by getMemberCount(membersState)
|
||||
val canInvite by getCanInvite(membersState)
|
||||
val canEditName by getCanSendStateEvent(membersState, StateEventType.ROOM_NAME)
|
||||
val canEditAvatar by getCanSendStateEvent(membersState, StateEventType.ROOM_AVATAR)
|
||||
|
|
@ -85,7 +82,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
roomAlias = room.alias,
|
||||
roomAvatarUrl = room.avatarUrl,
|
||||
roomTopic = topicState,
|
||||
memberCount = memberCount,
|
||||
memberCount = room.joinedMemberCount,
|
||||
isEncrypted = room.isEncrypted,
|
||||
canInvite = canInvite,
|
||||
canEdit = canEditAvatar || canEditName || canEditTopic,
|
||||
|
|
@ -131,18 +128,4 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
}
|
||||
return canSendEvent
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getMemberCount(membersState: MatrixRoomMembersState): State<Async<Int>> {
|
||||
return remember(membersState) {
|
||||
derivedStateOf {
|
||||
when (membersState) {
|
||||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
||||
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.roomdetails.impl
|
|||
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
|
||||
data class RoomDetailsState(
|
||||
|
|
@ -27,7 +26,7 @@ data class RoomDetailsState(
|
|||
val roomAlias: String?,
|
||||
val roomAvatarUrl: String?,
|
||||
val roomTopic: RoomTopicState,
|
||||
val memberCount: Async<Int>,
|
||||
val memberCount: Long,
|
||||
val isEncrypted: Boolean,
|
||||
val roomType: RoomDetailsType,
|
||||
val roomMemberDetailsState: RoomMemberDetailsState?,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.roomdetails.impl
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
|
|
@ -32,7 +31,6 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
|||
aRoomDetailsState().copy(roomTopic = RoomTopicState.CanAddTopic),
|
||||
aRoomDetailsState().copy(isEncrypted = false),
|
||||
aRoomDetailsState().copy(roomAlias = null),
|
||||
aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())),
|
||||
aDmRoomDetailsState().copy(roomName = "Daniel"),
|
||||
aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"),
|
||||
aRoomDetailsState().copy(canInvite = true),
|
||||
|
|
@ -73,7 +71,7 @@ fun aRoomDetailsState() = RoomDetailsState(
|
|||
"|| MAI iki/Marketing " +
|
||||
"|| MAI iki/Marketing..."
|
||||
),
|
||||
memberCount = Async.Success(32),
|
||||
memberCount = 32,
|
||||
isEncrypted = true,
|
||||
canInvite = false,
|
||||
canEdit = false,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
|||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||
import io.element.android.libraries.architecture.isLoading
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -145,10 +144,8 @@ fun RoomDetailsView(
|
|||
}
|
||||
|
||||
if (state.roomType is RoomDetailsType.Room) {
|
||||
val memberCount = state.memberCount.dataOrNull()
|
||||
MembersSection(
|
||||
memberCount = memberCount,
|
||||
isLoading = state.memberCount.isLoading(),
|
||||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
|
||||
|
|
@ -273,8 +270,7 @@ internal fun TopicSection(
|
|||
|
||||
@Composable
|
||||
internal fun MembersSection(
|
||||
memberCount: Int?,
|
||||
isLoading: Boolean,
|
||||
memberCount: Long,
|
||||
openRoomMemberList: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -282,9 +278,8 @@ internal fun MembersSection(
|
|||
PreferenceText(
|
||||
title = stringResource(R.string.screen_room_details_people_title),
|
||||
icon = Icons.Outlined.Person,
|
||||
currentValue = memberCount?.toString(),
|
||||
currentValue = memberCount.toString(),
|
||||
onClick = openRoomMemberList,
|
||||
loadingCurrentValue = isLoading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ open class RoomDetailsEditStateProvider : PreviewParameterProvider<RoomDetailsEd
|
|||
get() = sequenceOf(
|
||||
aRoomDetailsEditState(),
|
||||
aRoomDetailsEditState().copy(roomTopic = ""),
|
||||
aRoomDetailsEditState().copy(roomAvatarUrl = Uri.EMPTY),
|
||||
aRoomDetailsEditState().copy(roomAvatarUrl = Uri.parse("example://uri")),
|
||||
aRoomDetailsEditState().copy(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false),
|
||||
aRoomDetailsEditState().copy(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
|
||||
aRoomDetailsEditState().copy(saveAction = Async.Loading()),
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ fun RoomDetailsEditView(
|
|||
LabelledTextField(
|
||||
label = stringResource(id = R.string.screen_room_details_room_name_label),
|
||||
value = state.roomName,
|
||||
placeholder = stringResource(id = StringR.string.common_room_name_placeholder),
|
||||
placeholder = stringResource(StringR.string.common_room_name_placeholder),
|
||||
singleLine = true,
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomName(it)) },
|
||||
)
|
||||
|
|
@ -160,9 +160,9 @@ fun RoomDetailsEditView(
|
|||
|
||||
if (state.canChangeTopic) {
|
||||
LabelledTextField(
|
||||
label = stringResource(id = StringR.string.common_topic),
|
||||
label = stringResource(StringR.string.common_topic),
|
||||
value = state.roomTopic,
|
||||
placeholder = stringResource(id = StringR.string.common_topic_placeholder),
|
||||
placeholder = stringResource(StringR.string.common_topic_placeholder),
|
||||
maxLines = 10,
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
|||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -40,6 +40,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||
|
|
@ -181,7 +182,7 @@ private fun RoomInviteMembersSearchBar(
|
|||
)
|
||||
|
||||
LazyColumn {
|
||||
items(results) { invitableUser ->
|
||||
itemsIndexed(results) { index, invitableUser ->
|
||||
if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
|
|
@ -208,6 +209,10 @@ private fun RoomInviteMembersSearchBar(
|
|||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (index < results.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ private fun LazyListScope.roomMemberListSection(
|
|||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
text = headerText(),
|
||||
fontSize = 16.sp,
|
||||
style = ElementTextStyles.Regular.callout,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Start,
|
||||
|
|
|
|||
|
|
@ -5,17 +5,25 @@
|
|||
<item quantity="few">"%1$d osoby"</item>
|
||||
<item quantity="other">"%1$d osob"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_add_topic_title">"Přidat téma"</string>
|
||||
<string name="screen_room_details_already_a_member">"Již členem"</string>
|
||||
<string name="screen_room_details_already_invited">"Již pozván(a)"</string>
|
||||
<string name="screen_room_details_edit_room_title">"Upravit místnost"</string>
|
||||
<string name="screen_room_details_edition_error">"Nepodařilo se nám aktualizovat všechny informace o této místnosti."</string>
|
||||
<string name="screen_room_details_edition_error_title">"Nelze aktualizovat místnost"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Šifrování zpráv povoleno"</string>
|
||||
<string name="screen_room_details_room_name_label">"Název místnosti"</string>
|
||||
<string name="screen_room_details_share_room_title">"Sdílet místnost"</string>
|
||||
<string name="screen_room_details_updating_room">"Aktualizace místnosti…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"Nevyřízeno"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Členové místnosti"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Zablokovat"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny zprávy od nich budou skryty. Tuto akci můžete kdykoli vrátit zpět."</string>
|
||||
<string name="screen_dm_details_block_user">"Zablokovat uživatele"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Odblokovat"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Po odblokování uživatele budete moci opět zobrazit všechny jeho zprávy."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Odblokovat uživatele"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Pozvat lidi"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Opustit místnost"</string>
|
||||
<string name="screen_room_details_people_title">"Lidé"</string>
|
||||
<string name="screen_room_details_security_title">"Zabezpečení"</string>
|
||||
|
|
|
|||
|
|
@ -4,17 +4,21 @@
|
|||
<item quantity="one">"1 Person"</item>
|
||||
<item quantity="other">"%1$d Personen"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_details_add_topic_title">"Thema hinzufügen"</string>
|
||||
<string name="screen_room_details_already_a_member">"Bereits Mitglied"</string>
|
||||
<string name="screen_room_details_already_invited">"Bereits eingeladen"</string>
|
||||
<string name="screen_room_details_edit_room_title">"Raum bearbeiten"</string>
|
||||
<string name="screen_room_details_edition_error">"Wir konnten nicht alle Informationen für diesen Raum aktualisieren."</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Nachrichtenverschlüsselung aktiviert"</string>
|
||||
<string name="screen_room_details_share_room_title">"Raum teilen"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Raummitglieder"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen."</string>
|
||||
<string name="screen_dm_details_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Wenn du den Benutzer entsperrst, kannst du wieder alle Nachrichten von ihm sehen."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Nutzer entblockieren"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Personen einladen"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Raum verlassen"</string>
|
||||
<string name="screen_room_details_people_title">"Personen"</string>
|
||||
<string name="screen_room_details_security_title">"Sicherheit"</string>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitar gente"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Salir de la sala"</string>
|
||||
<string name="screen_room_details_people_title">"Personas"</string>
|
||||
<string name="screen_room_details_security_title">"Seguridad"</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<string name="screen_dm_details_unblock_alert_action">"Débloquer"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Lorsque vous débloquez l\'utilisateur, vous pourrez à nouveau voir tous leur messages."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Débloquer l\'utilisateur"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Inviter des personnes"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Quitter la salle"</string>
|
||||
<string name="screen_room_details_people_title">"Personnes"</string>
|
||||
<string name="screen_room_details_security_title">"Sécurité"</string>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
</plurals>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
|
||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Esci dalla stanza"</string>
|
||||
<string name="screen_room_details_people_title">"Persone"</string>
|
||||
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<string name="screen_room_details_edition_error">"A apărut o eroare la actualizarea detaliilor camerei"</string>
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||
<string name="screen_room_details_updating_room">"Se actualizează camera…"</string>
|
||||
<string name="screen_room_member_list_pending_header_title">"În așteptare"</string>
|
||||
|
|
@ -20,7 +21,6 @@
|
|||
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
|
||||
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Părăsiți camera"</string>
|
||||
<string name="screen_room_details_people_title">"Persoane"</string>
|
||||
<string name="screen_room_details_security_title">"Securitate"</string>
|
||||
|
|
|
|||
|
|
@ -26,18 +26,15 @@ import io.element.android.features.roomdetails.impl.RoomDetailsType
|
|||
import io.element.android.features.roomdetails.impl.RoomTopicState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_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.room.FakeMatrixRoom
|
||||
|
|
@ -69,48 +66,13 @@ class RoomDetailsPresenterTests {
|
|||
assertThat(initialState.roomName).isEqualTo(room.name)
|
||||
assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
|
||||
assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))
|
||||
assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
|
||||
assertThat(initialState.memberCount).isEqualTo(room.joinedMemberCount)
|
||||
assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - room member count is calculated asynchronously`() = runTest {
|
||||
val error = RuntimeException()
|
||||
val room = aMatrixRoom()
|
||||
val roomMembers = listOf(
|
||||
aRoomMember(A_USER_ID),
|
||||
aRoomMember(A_USER_ID_2, membership = RoomMembershipState.INVITE),
|
||||
)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
|
||||
skipItems(1)
|
||||
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Pending(null))
|
||||
val loadingState = awaitItem()
|
||||
assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null))
|
||||
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Error(error))
|
||||
skipItems(1)
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null))
|
||||
|
||||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||
skipItems(1)
|
||||
val successState = awaitItem()
|
||||
assertThat(successState.memberCount).isEqualTo(Async.Success(1))
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state with no room name`() = runTest {
|
||||
val room = aMatrixRoom(name = null)
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -47,16 +49,19 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
|
|
@ -68,12 +73,13 @@ import io.element.android.libraries.designsystem.ElementTextStyles
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.noFontPadding
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||
|
|
@ -212,37 +218,48 @@ fun RoomListContent(
|
|||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
|
||||
TextButton(
|
||||
content = {
|
||||
Text(stringResource(StringR.string.action_invites_list))
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.roomListUnreadIndicator())
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onInvitesClicked,
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.heightIn(min = 48.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(StringR.string.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = noFontPadding,
|
||||
)
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.roomListUnreadIndicator())
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
itemsIndexed(
|
||||
items = state.roomList,
|
||||
contentType = { room -> room.contentType() },
|
||||
) { room ->
|
||||
contentType = { _, room -> room.contentType() },
|
||||
) { index, room ->
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
if (index != state.roomList.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue