Merge branch 'develop' into renovate/accompanist
This commit is contained in:
commit
49ce8b12e7
1401 changed files with 5847 additions and 3685 deletions
|
|
@ -66,7 +66,7 @@ else
|
|||
fi
|
||||
|
||||
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
|
||||
git config user.email "benoitm+elementbot@element.io"
|
||||
git config user.email "android@element.io"
|
||||
else
|
||||
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
|
||||
fi
|
||||
|
|
|
|||
36
CHANGES.md
36
CHANGES.md
|
|
@ -1,3 +1,39 @@
|
|||
Changes in Element X v0.6.4 (2024-09-25)
|
||||
========================================
|
||||
|
||||
### 🙌 Improvements
|
||||
* Pinned messages : add pin icon in timeline for pinned events. by @ganfra in https://github.com/element-hq/element-x-android/pull/3500
|
||||
* Include inviter in the notification for invitation by @bmarty in https://github.com/element-hq/element-x-android/pull/3503
|
||||
|
||||
### 🐛 Bugfixes
|
||||
* Fix crash when session is deleted on another client by @bmarty in https://github.com/element-hq/element-x-android/pull/3515
|
||||
* Fix pinned events banner reappearing when loading by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3519
|
||||
* Fix various crashes by @bmarty in https://github.com/element-hq/element-x-android/pull/3533
|
||||
* Perform the migration, even if the current version is not known. by @bmarty in https://github.com/element-hq/element-x-android/pull/3535
|
||||
* timeline : makes sure to emit empty list if initial reset has no item. by @ganfra in https://github.com/element-hq/element-x-android/pull/3538
|
||||
|
||||
### 🗣 Translations
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3513
|
||||
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3517
|
||||
|
||||
### Dependency upgrades
|
||||
* Update dependency io.nlopez.compose.rules:detekt to v0.4.12 by @renovate in https://github.com/element-hq/element-x-android/pull/3436
|
||||
* Update dependency com.posthog:posthog-android to v3.7.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3443
|
||||
* Update dependency com.otaliastudios:transcoder to v0.11.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3440
|
||||
* Update dependency org.maplibre.gl:android-sdk to v11.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3408
|
||||
* Update dependencyAnalysis to v2.0.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3508
|
||||
* Update dependency org.maplibre.gl:android-sdk-ktx-v7 to v3.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3507
|
||||
* Update dependencyAnalysis to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3526
|
||||
* Update dependency net.java.dev.jna:jna to v5.15.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3525
|
||||
* Update dependency androidx.startup:startup-runtime to v1.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3516
|
||||
* dependencies : update rust sdk to 0.2.48 by @ganfra in https://github.com/element-hq/element-x-android/pull/3532
|
||||
|
||||
### Others
|
||||
* Change ElementBot mail to android@element.io by @bmarty in https://github.com/element-hq/element-x-android/pull/3497
|
||||
* Test RustMatrixClient and other classes in the matrix module by @bmarty in https://github.com/element-hq/element-x-android/pull/3501
|
||||
* Pinned messages analytics by @ganfra in https://github.com/element-hq/element-x-android/pull/3523
|
||||
* Remove ability to configure default log level by @bmarty in https://github.com/element-hq/element-x-android/pull/3531
|
||||
|
||||
Changes in Element X v0.6.3 (2024-09-19)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -250,10 +250,12 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
anvil(projects.anvilcodegen)
|
||||
|
||||
// Comment to not include firebase in the project
|
||||
"gplayImplementation"(projects.libraries.pushproviders.firebase)
|
||||
// Comment to not include unified push in the project
|
||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
|
||||
"gplayImplementation"(projects.libraries.pushproviders.firebase)
|
||||
}
|
||||
if (ModulesConfig.pushProvidersConfig.includeUnifiedPush) {
|
||||
implementation(projects.libraries.pushproviders.unifiedpush)
|
||||
}
|
||||
|
||||
implementation(libs.appyx.core)
|
||||
implementation(libs.androidx.splash)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.architecture.bindings
|
|||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.x.di.AppBindings
|
||||
import io.element.android.x.intent.SafeUriHandler
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -64,6 +65,7 @@ class MainActivity : NodeActivity() {
|
|||
CompositionLocalProvider(
|
||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||
LocalUriHandler provides SafeUriHandler(this),
|
||||
LocalAnalyticsService provides appBindings.analyticsService(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.tracing.TracingService
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface AppBindings {
|
||||
|
|
@ -32,4 +33,6 @@ interface AppBindings {
|
|||
fun migrationEntryPoint(): MigrationEntryPoint
|
||||
|
||||
fun lockScreenEntryPoint(): LockScreenEntryPoint
|
||||
|
||||
fun analyticsService(): AnalyticsService
|
||||
}
|
||||
|
|
|
|||
4
appnav/src/main/res/values-be/translations.xml
Normal file
4
appnav/src/main/res/values-be/translations.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
|
||||
</resources>
|
||||
|
|
@ -48,7 +48,7 @@ allprojects {
|
|||
config.from(files("$rootDir/tools/detekt/detekt.yml"))
|
||||
}
|
||||
dependencies {
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.11")
|
||||
detektPlugins("io.nlopez.compose.rules:detekt:0.4.12")
|
||||
}
|
||||
|
||||
// KtLint
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/40006040.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40006040.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: mainly bug fixes.
|
||||
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||
|
|
@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -36,6 +35,7 @@ import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrgani
|
|||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.background.OnboardingBackground
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
|
@ -104,14 +104,9 @@ private fun AnalyticsOptInHeader(
|
|||
bold = true,
|
||||
tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK,
|
||||
)
|
||||
ClickableText(
|
||||
text = text,
|
||||
onClick = {
|
||||
text
|
||||
.getStringAnnotations(LINK_TAG, it, it)
|
||||
.firstOrNull()
|
||||
?.let { _ -> onClickTerms() }
|
||||
},
|
||||
ClickableLinkText(
|
||||
annotatedString = text,
|
||||
onClick = { onClickTerms() },
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
<string name="screen_analytics_prompt_read_terms">"Sa võid lugeda meie kasutustingimusi %1$s"</string>
|
||||
<string name="screen_analytics_prompt_read_terms_content_link">"siin"</string>
|
||||
<string name="screen_analytics_prompt_settings">"Selle valiku saad igal ajal välja lülitada"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga andmeid kolmandate osapooltega"</string>
|
||||
<string name="screen_analytics_prompt_title">"Aita parandada %1$s rakendust"</string>
|
||||
<string name="screen_analytics_prompt_third_party_sharing">"Me ei jaga sinu andmeid kolmandate osapooltega"</string>
|
||||
<string name="screen_analytics_prompt_title">"Aita parandada rakendust %1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import io.element.android.features.call.impl.utils.WidgetMessageInterceptor
|
|||
|
||||
sealed interface CallScreenEvents {
|
||||
data object Hangup : CallScreenEvents
|
||||
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) :
|
||||
CallScreenEvents
|
||||
data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents
|
||||
data class OnWebViewError(val description: String?) : CallScreenEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
|
||||
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
|
||||
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
|
||||
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
|
||||
var webViewError by remember { mutableStateOf<String?>(null) }
|
||||
val languageTag = languageTagProvider.provideLanguageTag()
|
||||
val theme = if (ElementTheme.isLightTheme) "light" else "dark"
|
||||
DisposableEffect(Unit) {
|
||||
|
|
@ -125,6 +127,8 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
LaunchedEffect(Unit) {
|
||||
interceptor.interceptedMessages
|
||||
.onEach {
|
||||
// We are receiving messages from the WebView, consider that the application is loaded
|
||||
ignoreWebViewError = true
|
||||
// Relay message to Widget Driver
|
||||
callWidgetDriver.value?.send(it)
|
||||
|
||||
|
|
@ -163,11 +167,18 @@ class CallScreenPresenter @AssistedInject constructor(
|
|||
is CallScreenEvents.SetupMessageChannels -> {
|
||||
messageInterceptor.value = event.widgetMessageInterceptor
|
||||
}
|
||||
is CallScreenEvents.OnWebViewError -> {
|
||||
if (!ignoreWebViewError) {
|
||||
webViewError = event.description.orEmpty()
|
||||
}
|
||||
// Else ignore the error, give a chance the Element Call to recover by itself.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CallScreenState(
|
||||
urlState = urlState.value,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = { handleEvents(it) },
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||
|
||||
data class CallScreenState(
|
||||
val urlState: AsyncData<String>,
|
||||
val webViewError: String?,
|
||||
val userAgent: String,
|
||||
val isInWidgetMode: Boolean,
|
||||
val eventSink: (CallScreenEvents) -> Unit,
|
||||
|
|
|
|||
|
|
@ -16,17 +16,20 @@ open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
|||
aCallScreenState(),
|
||||
aCallScreenState(urlState = AsyncData.Loading()),
|
||||
aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))),
|
||||
aCallScreenState(webViewError = "Error details from WebView"),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aCallScreenState(
|
||||
urlState: AsyncData<String> = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
|
||||
webViewError: String? = null,
|
||||
userAgent: String = "",
|
||||
isInWidgetMode: Boolean = false,
|
||||
eventSink: (CallScreenEvents) -> Unit = {},
|
||||
): CallScreenState {
|
||||
return CallScreenState(
|
||||
urlState = urlState,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = eventSink,
|
||||
|
|
|
|||
|
|
@ -85,35 +85,48 @@ internal fun CallScreenView(
|
|||
BackHandler {
|
||||
handleBack()
|
||||
}
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
if (state.webViewError != null) {
|
||||
ErrorDialog(
|
||||
content = buildString {
|
||||
append(stringResource(CommonStrings.error_unknown))
|
||||
state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) }
|
||||
},
|
||||
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
|
||||
)
|
||||
} else {
|
||||
CallWebView(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.fillMaxSize(),
|
||||
url = state.urlState,
|
||||
userAgent = state.userAgent,
|
||||
onPermissionsRequest = { request ->
|
||||
val androidPermissions = mapWebkitPermissions(request.resources)
|
||||
val callback: RequestPermissionCallback = { request.grant(it) }
|
||||
requestPermissions(androidPermissions.toTypedArray(), callback)
|
||||
},
|
||||
onWebViewCreate = { webView ->
|
||||
val interceptor = WebViewWidgetMessageInterceptor(webView)
|
||||
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
|
||||
val pipController = WebViewPipController(webView)
|
||||
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
|
||||
url = state.urlState,
|
||||
userAgent = state.userAgent,
|
||||
onPermissionsRequest = { request ->
|
||||
val androidPermissions = mapWebkitPermissions(request.resources)
|
||||
val callback: RequestPermissionCallback = { request.grant(it) }
|
||||
requestPermissions(androidPermissions.toTypedArray(), callback)
|
||||
},
|
||||
onWebViewCreate = { webView ->
|
||||
val interceptor = WebViewWidgetMessageInterceptor(
|
||||
webView = webView,
|
||||
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
|
||||
)
|
||||
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
|
||||
val pipController = WebViewPipController(webView)
|
||||
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
|
||||
}
|
||||
)
|
||||
when (state.urlState) {
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading ->
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
|
||||
is AsyncData.Failure ->
|
||||
ErrorDialog(
|
||||
content = state.urlState.error.message.orEmpty(),
|
||||
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
|
||||
)
|
||||
is AsyncData.Success -> Unit
|
||||
}
|
||||
)
|
||||
when (state.urlState) {
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Loading ->
|
||||
ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait))
|
||||
is AsyncData.Failure ->
|
||||
ErrorDialog(
|
||||
content = state.urlState.error.message.orEmpty(),
|
||||
onSubmit = { state.eventSink(CallScreenEvents.Hangup) },
|
||||
)
|
||||
is AsyncData.Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,7 +281,11 @@ class ElementCallActivity :
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun enterPipMode(): Boolean {
|
||||
return enterPictureInPictureMode(getPictureInPictureParams())
|
||||
return if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
enterPictureInPictureMode(getPictureInPictureParams())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
|
|
|
|||
|
|
@ -8,16 +8,23 @@
|
|||
package io.element.android.features.call.impl.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.webkit.WebViewCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import io.element.android.features.call.impl.BuildConfig
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import timber.log.Timber
|
||||
|
||||
class WebViewWidgetMessageInterceptor(
|
||||
private val webView: WebView,
|
||||
private val onError: (String?) -> Unit,
|
||||
) : WidgetMessageInterceptor {
|
||||
companion object {
|
||||
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this
|
||||
|
|
@ -45,16 +52,35 @@ class WebViewWidgetMessageInterceptor(
|
|||
if (message.data.response && message.data.api == "toWidget"
|
||||
|| !message.data.response && message.data.api == "fromWidget") {
|
||||
let json = JSON.stringify(event.data)
|
||||
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } }
|
||||
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG }}
|
||||
$LISTENER_NAME.postMessage(json);
|
||||
} else {
|
||||
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } }
|
||||
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG }}
|
||||
}
|
||||
});
|
||||
""".trimIndent(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
||||
// No network for instance, transmit the error
|
||||
Timber.e("onReceivedError error: ${error?.errorCode} ${error?.description}")
|
||||
onError(error?.description?.toString())
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) {
|
||||
Timber.e("onReceivedHttpError error: ${errorResponse?.statusCode} ${errorResponse?.reasonPhrase}")
|
||||
onError(errorResponse?.statusCode.toString())
|
||||
super.onReceivedHttpError(view, request, errorResponse)
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||
Timber.e("onReceivedSslError error: ${error?.primaryError}")
|
||||
onError(error?.primaryError?.toString())
|
||||
super.onReceivedSslError(view, handler, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a WebMessageListener, which will receive messages from the WebView and reply to them
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class CallScreenPresenterTest {
|
|||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
|
||||
assertThat(initialState.webViewError).isNull()
|
||||
assertThat(initialState.isInWidgetMode).isFalse()
|
||||
analyticsLambda.assertions().isNeverCalled()
|
||||
joinedCallLambda.assertions().isCalledOnce()
|
||||
|
|
@ -270,6 +271,48 @@ class CallScreenPresenterTest {
|
|||
assert(stopSyncLambda).isCalledOnce()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - error from WebView are updating the state`() = runTest {
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.ExternalUrl("https://call.element.io"),
|
||||
activeCallManager = FakeActiveCallManager(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Wait until the URL is loaded
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.webViewError).isEqualTo("A Webview error")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - error from WebView are ignored if Element Call is loaded`() = runTest {
|
||||
val presenter = createCallScreenPresenter(
|
||||
callType = CallType.ExternalUrl("https://call.element.io"),
|
||||
activeCallManager = FakeActiveCallManager(),
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
// Wait until the URL is loaded
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
|
||||
val messageInterceptor = FakeWidgetMessageInterceptor()
|
||||
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
|
||||
// Emit a message
|
||||
messageInterceptor.givenInterceptedMessage("A message")
|
||||
// WebView emits an error, but it will be ignored
|
||||
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.webViewError).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createCallScreenPresenter(
|
||||
callType: CallType,
|
||||
navigator: CallScreenNavigator = FakeCallScreenNavigator(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Smazat všechny mé zprávy"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace."</string>
|
||||
<string name="screen_deactivate_account_description">"Deaktivace vašeho účtu je %1$s, což způsobí:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"nezvratná"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s váš účet (nemůžete se znovu přihlásit a vaše ID nelze znovu použít)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Trvale zakázat"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Odebere vás ze všech chatovacích místností."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Odstraní informace o vašem účtu z našeho serveru identit."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Vaše zprávy budou stále viditelné registrovaným uživatelům, ale nebudou dostupné novým ani neregistrovaným uživatelům, pokud se rozhodnete je smazat."</string>
|
||||
<string name="screen_deactivate_account_title">"Deaktivovat účet"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Παρακαλώ επιβεβαίωσε ότι θες να απενεργοποιήσεις τον λογαριασμό σου. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Διαγραφή όλων των μηνυμάτων μου"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Προειδοποίηση: Οι μελλοντικοί χρήστες ενδέχεται να βλέπουν ελλιπείς συνομιλίες."</string>
|
||||
<string name="screen_deactivate_account_description">"Η απενεργοποίηση του λογαριασμού σας είναι %1$s, θα:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"μη αναστρέψιμο"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s τον λογαριασμό σου (δεν μπορείς να συνδεθείς ξανά και το αναγνωριστικό σου δεν μπορεί να επαναχρησιμοποιηθεί)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Μόνιμη απενεργοποίηση"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Σε αφαιρέσει από όλα τα δωμάτια συνομιλίας."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Διαγράψει τα στοιχεία του λογαριασμού σου από τον διακομιστή ταυτότητάς μας."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Τα μηνύματά σου θα εξακολουθούν να είναι ορατά στους εγγεγραμμένους χρήστες, αλλά δεν θα είναι διαθέσιμα σε νέους ή μη εγγεγραμμένους χρήστες εάν επιλέξεις να τα διαγράψεις."</string>
|
||||
<string name="screen_deactivate_account_title">"Απενεργοποίηση λογαριασμού"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Kustuta kõik minu sõnumid"</string>
|
||||
<string name="screen_deactivate_account_title">"Eemalda konto kasutusest"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Veuillez confirmer que vous souhaitez désactiver votre compte. Cette action ne peut pas être annulée."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Supprimer tous mes messages"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Attention : les futurs utilisateurs pourraient voir des conversations incomplètes."</string>
|
||||
<string name="screen_deactivate_account_description">"La désactivation de votre compte est %1$s, cela va:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"irréversible"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s votre compte (vous ne pourrez plus vous reconnecter et votre identifiant ne pourra pas être réutilisé)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Désactiver définitivement"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Vous retirer de tous les salons et toutes les discussions."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Supprimer les informations de votre compte du serveur d’identité."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés."</string>
|
||||
<string name="screen_deactivate_account_title">"Désactiver votre compte"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Erősítse meg, hogy deaktiválja a fiókját. Ez a művelet nem vonható vissza."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Összes saját üzenet törlése"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Figyelmeztetés: A jövőbeli felhasználók hiányos beszélgetéseket láthatnak."</string>
|
||||
<string name="screen_deactivate_account_description">"A fiók deaktiválása %1$s, a következőket okozza:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"visszafordíthatatlan"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Véglegesen letiltja"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Eltávolításra kerül az összes csevegőszobából."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Törlésre kerülnek a fiókadatai a személyazonosító kiszolgálónkról."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket."</string>
|
||||
<string name="screen_deactivate_account_title">"Fiók deaktiválása"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Подтвердите, что вы хотите деактивировать свою учетную запись. Это действие не может быть отменено."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Удалить все мои сообщения"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Предупреждение: будущие пользователи могут увидеть незавершенные разговоры."</string>
|
||||
<string name="screen_deactivate_account_description">"Деактивация вашей учетной записи %1$s означает следующее:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"необратимый"</string>
|
||||
<string name="screen_deactivate_account_list_item_1">"%1$s вашей учетной записи (вы не можете войти в систему снова, и ваш ID не может быть использован повторно)."</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Отключить навсегда"</string>
|
||||
<string name="screen_deactivate_account_list_item_2">"Удалите вас из всех чатов."</string>
|
||||
<string name="screen_deactivate_account_list_item_3">"Удалите данные своей учетной записи с нашего сервера идентификации."</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их."</string>
|
||||
<string name="screen_deactivate_account_title">"Отключить учётную запись"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_confirmation_dialog_content">"Prosím potvrďte, že chcete deaktivovať svoj účet. Túto akciu nie je možné vrátiť späť."</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages">"Vymazať všetky moje správy"</string>
|
||||
<string name="screen_deactivate_account_delete_all_messages_notice">"Upozornenie: Budúcim používateľom sa môžu zobraziť neúplné konverzácie."</string>
|
||||
<string name="screen_deactivate_account_description">"Deaktivácia vášho účtu znamená %1$s, že:"</string>
|
||||
<string name="screen_deactivate_account_description_bold_part">"nezvratný"</string>
|
||||
<string name="screen_deactivate_account_list_item_1_bold_part">"Natrvalo zakázať"</string>
|
||||
<string name="screen_deactivate_account_list_item_4">"Vaše správy budú stále viditeľné pre registrovaných používateľov, ale nebudú dostupné pre nových alebo neregistrovaných používateľov, ak sa ich rozhodnete odstrániť."</string>
|
||||
<string name="screen_deactivate_account_title">"Deaktivovať účet"</string>
|
||||
</resources>
|
||||
|
|
@ -18,10 +18,10 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
||||
import io.element.android.features.lockscreen.impl.R
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästa aega ja kasuta alati %1$s rakenduse lukustuse eemaldamiseks"</string>
|
||||
<string name="screen_app_lock_setup_choose_pin">"Vali PIN-kood"</string>
|
||||
<string name="screen_app_lock_setup_confirm_pin">"Korda PIN-koodi"</string>
|
||||
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
|
||||
<string name="screen_app_lock_setup_pin_context">"Lisamaks oma %1$s rakenduse vestlustele turvalisust ja privaatsust, lukusta oma nutiseade.
|
||||
|
||||
Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvakaalutlustel logitakse sind rakendusest välja."</string>
|
||||
<string name="screen_app_lock_setup_pin_forbidden_dialog_content">"Turvakaalutlustel sa ei saa sellist PIN-koodi kasutada"</string>
|
||||
|
|
@ -31,7 +31,7 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka
|
|||
<item quantity="one">"Vale PIN-kood. Saad proovida veel %1$d korra"</item>
|
||||
<item quantity="other">"Vale PIN-kood. Saad proovida veel %1$d korda"</item>
|
||||
</plurals>
|
||||
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetrilist tuvastust"</string>
|
||||
<string name="screen_app_lock_use_biometric_android">"Kasuta biomeetriat"</string>
|
||||
<string name="screen_app_lock_use_pin_android">"Kasuta PIN-koodi"</string>
|
||||
<string name="screen_signout_in_progress_dialog_content">"Logime välja…"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<string name="screen_change_server_form_notice">"Вы можаце падключыцца толькі да існуючага сервера, які падтрымлівае sliding sync. Адміністратару хатняга сервера запатрабуецца наладзіць яго. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Які адрас вашага сервера?"</string>
|
||||
<string name="screen_change_server_title">"Выберыце свой сервер"</string>
|
||||
<string name="screen_create_account_title">"Стварыць уліковы запіс"</string>
|
||||
<string name="screen_login_error_deactivated_account">"Гэты ўліковы запіс быў дэактываваны."</string>
|
||||
<string name="screen_login_error_invalid_credentials">"Няправільнае імя карыстальніка і/або пароль"</string>
|
||||
<string name="screen_login_error_invalid_user_id">"Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: ‘@user:homeserver.org’"</string>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<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_change_server_title">"Vyberte váš server"</string>
|
||||
<string name="screen_create_account_title">"Vytvořit účet"</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>
|
||||
<string name="screen_login_error_invalid_user_id">"Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"</string>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<string name="screen_account_provider_form_notice">"Sisesta otsingusõna või domeeni nimi."</string>
|
||||
<string name="screen_account_provider_form_subtitle">"Otsi äriühingut, kogukonda või võrgus leiduvat Matrixi serverit."</string>
|
||||
<string name="screen_account_provider_form_title">"Leia teenusepakkuja"</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signin_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signin_title">"Sa oled sisse logimas %s teenusesse"</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signup_subtitle">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_account_provider_signup_title">"Sa oled loomas kasutajakontot %s teenuses"</string>
|
||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org on suur ja tasuta koduserver Matrixi võrgus, mis on mõeldud turvalise ja hajutatud suhtluse jaoks. Selle serveri halduse eest vastutab Matrix.org Foundation."</string>
|
||||
<string name="screen_change_account_provider_other">"Muu teenusepakkuja"</string>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<string name="screen_change_server_error_invalid_homeserver">"Me ei suutnud luuaühendust selle koduserveriga. Palun kontrolli, kas koduserveri aadress on õige. Kui aadress on õige, siis täiendavat teavet oskab sulle anda koduserveri haldaja."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Sliding sync režiim pole saadaval vea tõttu well-known failis:
|
||||
%1$s"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta Sliding sync režiimi"</string>
|
||||
<string name="screen_change_server_error_no_sliding_sync_message">"See koduserver hetkel ei toeta „Sliding sync“ režiimi"</string>
|
||||
<string name="screen_change_server_form_header">"Koduserveri url"</string>
|
||||
<string name="screen_change_server_form_notice">"Sa saad luua ühendust vaid olemasoleva serveriga, mis toetab Sliding sync režiimi. Sinu koduserveri haldur peaks selle seadistama. %1$s"</string>
|
||||
<string name="screen_change_server_subtitle">"Mis on sinu koduserveri aadress?"</string>
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Teine seade pole sisselogitud"</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_subtitle">"Sisselogimine katkestati teises seadmes."</string>
|
||||
<string name="screen_qr_code_login_error_cancelled_title">"Sisselogimispäring on tühistatud"</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teise seadmes keeldutud."</string>
|
||||
<string name="screen_qr_code_login_error_declined_subtitle">"Sisselogimisest on teises seadmes keeldutud."</string>
|
||||
<string name="screen_qr_code_login_error_declined_title">"Sisselogimisest on keeldutud"</string>
|
||||
<string name="screen_qr_code_login_error_expired_subtitle">"Sisselogimine aegus. Palun proovi uuesti."</string>
|
||||
<string name="screen_qr_code_login_error_expired_title">"Sisselogimine jäi etteantud aja jooksul tegemata"</string>
|
||||
|
|
@ -76,7 +76,7 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega."</strin
|
|||
<string name="screen_server_confirmation_change_server">"Muuda teenusepakujat"</string>
|
||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Privaatne server Elemendi töötajate jaoks."</string>
|
||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks."</string>
|
||||
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postitenuse pakkujat."</string>
|
||||
<string name="screen_server_confirmation_message_register">"See on koht, kus sinu vestlused elavad – just nagu kasutaksid oma e-kirjade säilitamiseks e-postiteenuse pakkujat."</string>
|
||||
<string name="screen_server_confirmation_title_login">"Sa oled sisselogimas koduserverisse %1$s"</string>
|
||||
<string name="screen_server_confirmation_title_register">"Sa oled loomas kasutajakontot koduserveris %1$s"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ dependencies {
|
|||
implementation(projects.libraries.uiUtils)
|
||||
implementation(projects.libraries.testtags)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.appconfig.MessageComposerConfig
|
||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
|
|
@ -77,6 +78,7 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
|
|||
import io.element.android.libraries.matrix.ui.room.canCall
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -104,6 +106,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val timelineController: TimelineController,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<MessagesState> {
|
||||
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
|
||||
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
|
||||
|
|
@ -285,6 +288,12 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.Timeline,
|
||||
kind = PinUnpinAction.Kind.Pin,
|
||||
)
|
||||
)
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
pinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
@ -296,6 +305,12 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.Timeline,
|
||||
kind = PinUnpinAction.Kind.Unpin,
|
||||
)
|
||||
)
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
unpinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -356,7 +356,7 @@ private fun EmojiReactionsRow(
|
|||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onCustomReactionClick,
|
||||
indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
|
||||
indication = ripple(bounded = false, radius = emojiRippleRadius),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
)
|
||||
|
|
@ -433,7 +433,7 @@ private fun EmojiButton(
|
|||
.clickable(
|
||||
enabled = true,
|
||||
onClick = { onClick(emoji) },
|
||||
indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
|
||||
indication = ripple(bounded = false, radius = emojiRippleRadius),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -106,7 +105,7 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
private fun PinnedMessagesBannerItemsEffect(
|
||||
onItemsChange: (AsyncData<ImmutableList<PinnedMessagesBannerItem>>) -> Unit,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.text.AnnotatedString
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -51,6 +52,8 @@ import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndica
|
|||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesBannerView(
|
||||
|
|
@ -79,6 +82,7 @@ private fun PinnedMessagesBannerRow(
|
|||
onViewAllClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
||||
Row(
|
||||
modifier = modifier
|
||||
|
|
@ -88,6 +92,7 @@ private fun PinnedMessagesBannerRow(
|
|||
.heightIn(min = 64.dp)
|
||||
.clickable {
|
||||
if (state is PinnedMessagesBannerState.Loaded) {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerClick)
|
||||
onClick(state.currentPinnedMessage.eventId)
|
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
}
|
||||
|
|
@ -112,7 +117,13 @@ private fun PinnedMessagesBannerRow(
|
|||
message = state.formattedMessage(),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
ViewAllButton(state, onViewAllClick)
|
||||
ViewAllButton(
|
||||
state = state,
|
||||
onViewAllClick = {
|
||||
onViewAllClick()
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerViewAllButton)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.compose.runtime.setValue
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -39,9 +41,10 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -58,6 +61,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
) : Presenter<PinnedMessagesListState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -82,6 +86,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
userHasPermissionToSendMessage = false,
|
||||
userHasPermissionToSendReaction = false,
|
||||
isCallOngoing = false,
|
||||
// don't compute this value or the pin icon will be shown
|
||||
pinnedEventIds = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +134,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
TimelineItemAction.Unpin -> handleUnpinAction(targetEvent)
|
||||
TimelineItemAction.ViewInTimeline -> {
|
||||
targetEvent.eventId?.let { eventId ->
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageListViewTimeline)
|
||||
navigator.onViewInTimelineClick(eventId)
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +144,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun handleUnpinAction(targetEvent: TimelineItem.Event) {
|
||||
if (targetEvent.eventId == null) return
|
||||
analyticsService.capture(
|
||||
PinUnpinAction(
|
||||
from = PinUnpinAction.From.MessagePinningList,
|
||||
kind = PinUnpinAction.Kind.Unpin,
|
||||
)
|
||||
)
|
||||
timelineProvider.invokeOnTimeline {
|
||||
unpinEvent(targetEvent.eventId)
|
||||
.onFailure {
|
||||
|
|
@ -159,7 +172,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
private fun PinnedMessagesListEffect(onItemsChange: (AsyncData<ImmutableList<TimelineItem>>) -> Unit) {
|
||||
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||
|
|
@ -44,6 +45,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesListView(
|
||||
|
|
@ -57,7 +60,14 @@ fun PinnedMessagesListView(
|
|||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
PinnedMessagesListTopBar(state, onBackClick)
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
PinnedMessagesListTopBar(
|
||||
state = state,
|
||||
onBackClick = {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageBannerCloseListButton)
|
||||
onBackClick()
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
PinnedMessagesListContent(
|
||||
|
|
@ -67,8 +77,8 @@ fun PinnedMessagesListView(
|
|||
onLinkClick = onLinkClick,
|
||||
onErrorDismiss = onBackClick,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
|
||||
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,4 +67,5 @@ data class TimelineRoomInfo(
|
|||
val userHasPermissionToSendMessage: Boolean,
|
||||
val userHasPermissionToSendReaction: Boolean,
|
||||
val isCallOngoing: Boolean,
|
||||
val pinnedEventIds: List<EventId>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -240,10 +240,12 @@ internal fun aTimelineRoomInfo(
|
|||
name: String = "Room name",
|
||||
isDm: Boolean = false,
|
||||
userHasPermissionToSendMessage: Boolean = true,
|
||||
pinnedEventIds: List<EventId> = emptyList(),
|
||||
) = TimelineRoomInfo(
|
||||
isDm = isDm,
|
||||
name = name,
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = true,
|
||||
isCallOngoing = false,
|
||||
pinnedEventIds = pinnedEventIds,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@ fun TimelineView(
|
|||
Box(modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
reverseLayout = useReverseLayout,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
|
|
@ -145,6 +145,7 @@ fun TimelineView(
|
|||
key = { timelineItem -> timelineItem.identifier() },
|
||||
) { timelineItem ->
|
||||
TimelineItemRow(
|
||||
modifier = Modifier.animateItem(),
|
||||
timelineItem = timelineItem,
|
||||
timelineRoomInfo = state.timelineRoomInfo,
|
||||
renderReadReceipts = state.renderReadReceipts,
|
||||
|
|
@ -269,8 +270,8 @@ private fun BoxScope.TimelineScrollHelper(
|
|||
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
onClick = { jumpToBottom() },
|
||||
)
|
||||
}
|
||||
|
|
@ -297,8 +298,8 @@ private fun JumpToBottomButton(
|
|||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
imageVector = CompoundIcons.ArrowRight(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||
)
|
||||
|
|
@ -312,12 +313,18 @@ internal fun TimelineViewPreview(
|
|||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreview {
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
val timelineEvents = timelineItems.filterIsInstance<TimelineItem.Event>()
|
||||
val lastEventIdFromMe = timelineEvents.firstOrNull { it.isMine }?.eventId
|
||||
val lastEventIdFromOther = timelineEvents.firstOrNull { !it.isMine }?.eventId
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||
) {
|
||||
TimelineView(
|
||||
state = aTimelineState(
|
||||
timelineItems = timelineItems,
|
||||
timelineRoomInfo = aTimelineRoomInfo(
|
||||
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
|
||||
),
|
||||
focusedEventIndex = 0,
|
||||
),
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -40,6 +39,7 @@ import io.element.android.libraries.core.extensions.to01
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
|
|||
import io.element.android.libraries.testtags.testTag
|
||||
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.85f
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||
private val MIN_BUBBLE_WIDTH = 80.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -93,14 +93,6 @@ fun MessageEventBubble(
|
|||
}
|
||||
}
|
||||
|
||||
fun Modifier.offsetForItem(): Modifier {
|
||||
return when {
|
||||
state.isMine -> this
|
||||
state.timelineRoomInfo.isDm -> this
|
||||
else -> offset(x = BUBBLE_INCOMING_OFFSET)
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||
val backgroundBubbleColor = when {
|
||||
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
||||
|
|
@ -109,11 +101,8 @@ fun MessageEventBubble(
|
|||
val bubbleShape = bubbleShape()
|
||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||
Box(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||
.padding(start = avatarRadius, end = 16.dp)
|
||||
.offsetForItem()
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
|
|
@ -138,12 +127,15 @@ fun MessageEventBubble(
|
|||
Surface(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.messageBubble)
|
||||
.widthIn(min = 80.dp)
|
||||
.widthIn(
|
||||
min = MIN_BUBBLE_WIDTH,
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
||||
)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundBubbleColor,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -48,7 +48,7 @@ fun MessageStateEventContainer(
|
|||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundColor,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
|
|||
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.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -100,6 +101,8 @@ val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
|
|||
// Width of the transparent border around the sender avatar
|
||||
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
||||
|
||||
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
event: TimelineItem.Event,
|
||||
|
|
@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
|
|||
sender,
|
||||
message,
|
||||
reactions,
|
||||
pinIcon,
|
||||
) = createRefs()
|
||||
|
||||
// Sender
|
||||
|
|
@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
|
|||
modifier = Modifier
|
||||
.constrainAs(message) {
|
||||
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
|
||||
this.linkStartOrEnd(event)
|
||||
if (event.isMine) {
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
} else {
|
||||
val startMargin = if (timelineRoomInfo.isDm) 16.dp else 16.dp + BUBBLE_INCOMING_OFFSET
|
||||
start.linkTo(parent.start, margin = startMargin)
|
||||
}
|
||||
},
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -327,6 +336,27 @@ private fun TimelineItemEventRowContent(
|
|||
)
|
||||
}
|
||||
|
||||
// Pin icon
|
||||
val isEventPinned = timelineRoomInfo.pinnedEventIds.contains(event.eventId)
|
||||
if (isEventPinned) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PinSolid(),
|
||||
contentDescription = stringResource(CommonStrings.common_pinned),
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
modifier = Modifier
|
||||
.padding(1.dp)
|
||||
.size(16.dp)
|
||||
.constrainAs(pinIcon) {
|
||||
top.linkTo(message.top)
|
||||
if (event.isMine) {
|
||||
end.linkTo(message.start, margin = 8.dp)
|
||||
} else {
|
||||
start.linkTo(message.end, margin = 8.dp)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Reactions
|
||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||
TimelineItemReactionsView(
|
||||
|
|
@ -364,7 +394,7 @@ private fun MessageSenderInformation(
|
|||
senderAvatar: AvatarData,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val avatarColors = AvatarColorsProvider.provide(senderAvatar.id, ElementTheme.isLightTheme)
|
||||
val avatarColors = AvatarColorsProvider.provide(senderAvatar.id)
|
||||
Row(modifier = modifier) {
|
||||
Avatar(senderAvatar)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -61,7 +61,7 @@ fun EmojiItem(
|
|||
.clickable(
|
||||
enabled = true,
|
||||
onClick = { onSelectEmoji(item) },
|
||||
indication = rememberRipple(bounded = false, radius = emojiSize.toDp() / 2 + 10.dp),
|
||||
indication = ripple(bounded = false, radius = emojiSize.toDp() / 2 + 10.dp),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
.clearAndSetSemantics {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.voicemessages.VoiceMessageExcep
|
|||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runUpdatingState
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.ui.utils.time.formatShort
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
|
|
@ -126,8 +127,8 @@ class VoiceMessagePresenter @AssistedInject constructor(
|
|||
it
|
||||
},
|
||||
) {
|
||||
player.prepare().apply {
|
||||
player.play()
|
||||
player.prepare().flatMap {
|
||||
runCatching { player.play() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
<item quantity="other">"%1$d jututoa muudatust"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s ja veel %3$d huviline"</item>
|
||||
<item quantity="other">"%1$s, %2$s ja veel %3$d huvilist"</item>
|
||||
<item quantity="one">"%1$s, %2$s ja veel %3$d osaleja"</item>
|
||||
<item quantity="other">"%1$s, %2$s ja veel %3$d osalejat"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s kirjutab"</item>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
|
|
@ -896,6 +897,7 @@ class MessagesPresenterTest {
|
|||
fun `present - handle action pin`() = runTest {
|
||||
val successPinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||
val failurePinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val timeline = FakeTimeline()
|
||||
val room = FakeMatrixRoom(
|
||||
liveTimeline = timeline,
|
||||
|
|
@ -906,7 +908,7 @@ class MessagesPresenterTest {
|
|||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -923,6 +925,10 @@ class MessagesPresenterTest {
|
|||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent))
|
||||
assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -931,6 +937,7 @@ class MessagesPresenterTest {
|
|||
val successUnpinEventLambda = lambdaRecorder { _: EventId -> Result.success(true) }
|
||||
val failureUnpinEventLambda = lambdaRecorder { _: EventId -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val timeline = FakeTimeline()
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom(
|
||||
liveTimeline = timeline,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
|
|
@ -940,7 +947,7 @@ class MessagesPresenterTest {
|
|||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, analyticsService = analyticsService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -957,6 +964,10 @@ class MessagesPresenterTest {
|
|||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent))
|
||||
assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
|
||||
assertThat(awaitItem().snackbarMessage).isNotNull()
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1074,6 +1085,7 @@ class MessagesPresenterTest {
|
|||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||
timelineController = TimelineController(matrixRoom),
|
||||
permalinkParser = permalinkParser,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.features.messages.impl.pinned.list
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||
|
|
@ -30,6 +31,8 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo
|
|||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -142,7 +145,7 @@ class PinnedMessagesListPresenterTest {
|
|||
val successUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.success(true) }
|
||||
val failureUnpinEventLambda = lambdaRecorder { _: EventId? -> Result.failure<Boolean>(A_THROWABLE) }
|
||||
val pinnedEventsTimeline = createPinnedMessagesTimeline()
|
||||
|
||||
val analyticsService = FakeAnalyticsService()
|
||||
val room = FakeMatrixRoom(
|
||||
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
|
|
@ -151,7 +154,7 @@ class PinnedMessagesListPresenterTest {
|
|||
).apply {
|
||||
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
|
||||
}
|
||||
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
|
||||
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true, analyticsService = analyticsService)
|
||||
presenter.test {
|
||||
skipItems(3)
|
||||
val filledState = awaitItem() as PinnedMessagesListState.Filled
|
||||
|
|
@ -174,6 +177,11 @@ class PinnedMessagesListPresenterTest {
|
|||
assert(failureUnpinEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(AN_EVENT_ID))
|
||||
|
||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList),
|
||||
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.MessagePinningList)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +294,7 @@ class PinnedMessagesListPresenterTest {
|
|||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||
isFeatureEnabled: Boolean = true,
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
): PinnedMessagesListPresenter {
|
||||
val timelineProvider = PinnedEventsTimelineProvider(
|
||||
room = room,
|
||||
|
|
@ -302,6 +311,7 @@ class PinnedMessagesListPresenterTest {
|
|||
timelineProvider = timelineProvider,
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
actionListPresenterFactory = FakeActionListPresenter.Factory,
|
||||
analyticsService = analyticsService,
|
||||
appCoroutineScope = this,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,7 @@ class MigrationPresenter @Inject constructor(
|
|||
LaunchedEffect(migrationStoreVersion) {
|
||||
val migrationValue = migrationStoreVersion ?: return@LaunchedEffect
|
||||
if (migrationValue == -1) {
|
||||
// Fresh install, no migration needed
|
||||
Timber.d("Fresh install, no migration needed.")
|
||||
migrationStore.setApplicationMigrationVersion(lastMigration)
|
||||
return@LaunchedEffect
|
||||
Timber.d("Fresh install, or previous installed application did not have the migration mechanism.")
|
||||
}
|
||||
if (migrationValue == lastMigration) {
|
||||
Timber.d("Current app migration version: $migrationValue. No migration needed.")
|
||||
|
|
|
|||
|
|
@ -27,12 +27,9 @@ class MigrationPresenterTest {
|
|||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - no migration should occurs on fresh installation, and last version should be stored`() = runTest {
|
||||
fun `present - run all migrations on fresh installation, and last version should be stored`() = runTest {
|
||||
val migrations = (1..10).map { order ->
|
||||
FakeAppMigration(
|
||||
order = order,
|
||||
migrateLambda = LambdaNoParamRecorder(ensureNeverCalled = true) { },
|
||||
)
|
||||
FakeAppMigration(order = order)
|
||||
}
|
||||
val store = InMemoryMigrationStore(initialApplicationMigrationVersion = -1)
|
||||
val presenter = createPresenter(
|
||||
|
|
@ -44,12 +41,15 @@ class MigrationPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.migrationAction).isEqualTo(AsyncData.Uninitialized)
|
||||
skipItems(1)
|
||||
skipItems(migrations.size)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.migrationAction).isEqualTo(AsyncData.Success(Unit))
|
||||
}
|
||||
assertThat(store.applicationMigrationVersion().first()).isEqualTo(migrations.maxOf { it.order })
|
||||
}
|
||||
for (migration in migrations) {
|
||||
migration.migrateLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ private fun ElementCallCategory(
|
|||
validation = callUrlState.validator,
|
||||
onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error),
|
||||
displayValue = { value -> !isUsingDefaultUrl(value) },
|
||||
keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri),
|
||||
onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ open class ConfigureTracingStateProvider : PreviewParameterProvider<ConfigureTra
|
|||
|
||||
fun aConfigureTracingState() = ConfigureTracingState(
|
||||
targetsToLogLevel = persistentMapOf(
|
||||
Target.COMMON to LogLevel.INFO,
|
||||
Target.ELEMENT to LogLevel.INFO,
|
||||
Target.MATRIX_SDK_FFI to LogLevel.WARN,
|
||||
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.ERROR,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
|
||||
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
|
||||
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
|
||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vali kuidas sa soovid saada teavitusi"</string>
|
||||
<string name="screen_advanced_settings_developer_mode">"Arendaja valikud"</string>
|
||||
<string name="screen_advanced_settings_developer_mode_description">"Selle eelistuse sisselülitamisel lisanduvad rakendusse arendaja tööks vajalikud valikud."</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?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 jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
|
||||
<string name="crash_detection_dialog_content">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
|
||||
<string name="rageshake_detection_dialog_content">"Tundub, et sa raputad oma nutiseadet ägedalt. Kas sa soovid saata meile veateadet?"</string>
|
||||
<string name="settings_rageshake">"Seadme äge raputamine"</string>
|
||||
<string name="settings_rageshake_detection_threshold">"Tuvastamise lävi"</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
<string name="screen_bug_report_include_logs">"Luba logide saatmine"</string>
|
||||
<string name="screen_bug_report_include_screenshot">"Saada ekraanitõmmis"</string>
|
||||
<string name="screen_bug_report_logs_description">"Tõhusama veaotsingu nimel lisame sinu veateatele logid. Kui sa seda ei soovi, siis lülita antud valik välja."</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimatu kui seda kasutasid. Kas tahaksid selle kohta meile aruande saata?"</string>
|
||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s jooksis kokku viimati, kui seda kasutasid. Kas tahaksid selle kohta meile veateate saata?"</string>
|
||||
<string name="screen_bug_report_view_logs">"Vaata logisid"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ dependencies {
|
|||
implementation(projects.features.createroom.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.features.userprofile.shared)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(projects.services.analytics.compose)
|
||||
implementation(projects.features.poll.api)
|
||||
implementation(projects.features.messages.api)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot
|
|||
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
|
||||
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
|
||||
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BackstackWithOverlayBox
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.overlay.operation.show
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -125,7 +126,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun openAvatarPreview(name: String, url: String) {
|
||||
backstack.push(NavTarget.AvatarPreview(name, url))
|
||||
overlay.show(NavTarget.AvatarPreview(name, url))
|
||||
}
|
||||
|
||||
override fun openPollHistory() {
|
||||
|
|
@ -186,7 +187,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
is NavTarget.RoomMemberDetails -> {
|
||||
val callback = object : UserProfileNodeHelper.Callback {
|
||||
override fun openAvatarPreview(username: String, avatarUrl: String) {
|
||||
backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
|
||||
overlay.show(NavTarget.AvatarPreview(username, avatarUrl))
|
||||
}
|
||||
|
||||
override fun onStartDM(roomId: RoomId) {
|
||||
|
|
@ -252,6 +253,6 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
BackstackWithOverlayBox(modifier)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
|
|
@ -80,6 +81,8 @@ import io.element.android.libraries.matrix.ui.model.getAvatarData
|
|||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.compose.LocalAnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
|
|
@ -111,9 +114,9 @@ fun RoomDetailsView(
|
|||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
|
|
@ -270,8 +273,8 @@ private fun MainActionsSection(
|
|||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val roomNotificationSettings = state.roomNotificationSettings
|
||||
|
|
@ -330,8 +333,8 @@ private fun RoomHeaderSection(
|
|||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
|
|
@ -340,8 +343,8 @@ private fun RoomHeaderSection(
|
|||
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||
}.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
)
|
||||
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
|
||||
}
|
||||
|
|
@ -357,8 +360,8 @@ private fun DmHeaderSection(
|
|||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
DmAvatars(
|
||||
|
|
@ -509,6 +512,7 @@ private fun PinnedMessagesItem(
|
|||
pinnedMessagesCount: Int?,
|
||||
onPinnedMessagesClick: () -> Unit,
|
||||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
|
||||
|
|
@ -520,7 +524,10 @@ private fun PinnedMessagesItem(
|
|||
} else {
|
||||
ListItemContent.Text(pinnedMessagesCount.toString())
|
||||
},
|
||||
onClick = onPinnedMessagesClick,
|
||||
onClick = {
|
||||
analyticsService.captureInteraction(Interaction.Name.PinnedMessageRoomInfoButton)
|
||||
onPinnedMessagesClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -24,6 +23,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.roomdetails.impl.R
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
|
|
@ -102,8 +102,8 @@ private fun RoomSpecificNotificationSettingsView(
|
|||
underline = false,
|
||||
bold = true,
|
||||
)
|
||||
ClickableText(
|
||||
text = text,
|
||||
ClickableLinkText(
|
||||
annotatedString = text,
|
||||
onClick = {
|
||||
onShowGlobalNotifications()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
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_ROOM_TOPIC
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
|
|
@ -22,8 +24,8 @@ fun aMatrixRoom(
|
|||
roomId: RoomId = A_ROOM_ID,
|
||||
displayName: String = A_ROOM_NAME,
|
||||
rawName: String? = displayName,
|
||||
topic: String? = "A topic",
|
||||
avatarUrl: String? = "https://matrix.org/avatar.jpg",
|
||||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = AN_AVATAR_URL,
|
||||
isEncrypted: Boolean = true,
|
||||
isPublic: Boolean = true,
|
||||
isDirect: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ 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.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
|
|
@ -129,7 +132,12 @@ class RoomDetailsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - initial state is updated with roomInfo if it exists`() = runTest {
|
||||
val roomInfo = aRoomInfo(name = "A room name", topic = "A topic", avatarUrl = "https://matrix.org/avatar.jpg", pinnedEventIds = listOf(AN_EVENT_ID))
|
||||
val roomInfo = aRoomInfo(
|
||||
name = A_ROOM_NAME,
|
||||
topic = A_ROOM_TOPIC,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
pinnedEventIds = listOf(AN_EVENT_ID),
|
||||
)
|
||||
val room = aMatrixRoom(
|
||||
canInviteResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
|||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
|
|
@ -218,7 +219,10 @@ class RoomListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
val needsSlidingSyncMigration by produceState(false) {
|
||||
value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
|
||||
value = runCatching {
|
||||
// Note: this can fail when the session is destroyed from another client.
|
||||
client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync()
|
||||
}.getOrNull().orFalse()
|
||||
}
|
||||
return when {
|
||||
showEmpty -> RoomListContentState.Empty
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ 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.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -137,7 +137,7 @@ private fun RoomSummaryScaffoldRow(
|
|||
val clickModifier = Modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
onLongClick = { onLongClick(room) },
|
||||
indication = rememberRipple(),
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ fun RoomListFiltersView(
|
|||
val zIndex = (if (previousFilters.value.contains(filterWithSelection.filter)) state.filterSelectionStates.size else 0) - i.toFloat()
|
||||
RoomListFilterView(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.animateItem()
|
||||
.zIndex(zIndex),
|
||||
roomListFilter = filterWithSelection.filter,
|
||||
selected = filterWithSelection.isSelected,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_migrate_to_native_sliding_sync_action">"Выйсці і абнавіць"</string>
|
||||
<string name="banner_set_up_recovery_title">"Наладзіць аднаўленне"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
|
||||
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="confirm_recovery_key_banner_message">"Sinu vestluste varukoopia pole hetkel sünkroonis. Säilitamaks ligipääsu vestluse varukoopiale palun sisesta oma taastevõti."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Sisesta oma taastevõti"</string>
|
||||
<string name="full_screen_intent_banner_message">"Selleks, et sul ainsamgi tähtis kõne ei jääks märkamata, siis palun muuda oma nutiseadme seadistusi nii, et lukustusvaates oleksid täisekraani mõõtu teavitused."</string>
|
||||
<string name="full_screen_intent_banner_title">"Täiusta oma telefonikõnede kogemust"</string>
|
||||
<string name="full_screen_intent_banner_title">"Sinu tõhusad telefonikõned"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<string name="screen_create_new_recovery_key_list_item_4">"Выконвайце інструкцыі, каб стварыць новы ключ аднаўлення"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_5">"Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке"</string>
|
||||
<string name="screen_create_new_recovery_key_title">"Скіньце шыфраванне для вашага ўліковага запісу з дапамогай іншай прылады"</string>
|
||||
<string name="screen_encryption_reset_action_continue_reset">"Працягнуць скід"</string>
|
||||
<string name="screen_encryption_reset_bullet_1">"Дадзеныя вашага ўліковага запісу, кантакты, налады і спіс чатаў будуць захаваны"</string>
|
||||
<string name="screen_encryption_reset_bullet_2">"Вы страціце існуючую гісторыю паведамленняў"</string>
|
||||
<string name="screen_encryption_reset_bullet_3">"Вам трэба будзе зноў запэўніць ўсе вашы існуючыя прылады і кантакты"</string>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<string name="screen_session_verification_enter_recovery_key">"Sisesta taastevõti"</string>
|
||||
<string name="screen_session_verification_open_existing_session_subtitle">"Saamaks ligipääsu krüptitud sõnumite ajaloole tõesta et tegemist on sinuga."</string>
|
||||
<string name="screen_session_verification_open_existing_session_title">"Ava olemasolev sessioon"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Proovi uuesti verifitseerimist"</string>
|
||||
<string name="screen_session_verification_positive_button_canceled">"Proovi verifitseerimist uuesti"</string>
|
||||
<string name="screen_session_verification_positive_button_initial">"Ma olen valmis alustama"</string>
|
||||
<string name="screen_session_verification_positive_button_verifying_ongoing">"Ootame kinnitust sobivusele"</string>
|
||||
<string name="screen_session_verification_ready_subtitle">"Võrdle unikaalset emojide kombinatsiooni"</string>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ media3 = "1.4.1"
|
|||
camera = "1.3.4"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2024.08.00"
|
||||
compose_bom = "2024.09.02"
|
||||
composecompiler = "1.5.15"
|
||||
|
||||
# Coroutines
|
||||
|
|
@ -39,7 +39,7 @@ test_core = "1.6.1"
|
|||
#other
|
||||
coil = "2.7.0"
|
||||
datetime = "0.6.0"
|
||||
dependencyAnalysis = "2.0.1"
|
||||
dependencyAnalysis = "2.1.0"
|
||||
serialization_json = "1.6.3"
|
||||
showkase = "1.0.3"
|
||||
appyx = "1.4.0"
|
||||
|
|
@ -67,7 +67,7 @@ kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
|
|||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||
gms_google_services = "com.google.gms:google-services:4.4.2"
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:33.2.0"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:33.3.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
|||
|
||||
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
|
||||
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
|
||||
androidx_startup = "androidx.startup:startup-runtime:1.1.1"
|
||||
androidx_startup = "androidx.startup:startup-runtime:1.2.0"
|
||||
androidx_preference = "androidx.preference:preference:1.2.1"
|
||||
androidx_webkit = "androidx.webkit:webkit:1.11.0"
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
|||
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
|
||||
coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" }
|
||||
compound = { module = "io.element.android:compound-android", version = "0.0.7" }
|
||||
compound = { module = "io.element.android:compound-android", version = "0.1.0" }
|
||||
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
|
||||
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7"
|
||||
|
|
@ -162,7 +162,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
|
|||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.47"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.48"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
|
|
@ -171,13 +171,13 @@ sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions",
|
|||
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||
sqlite = "androidx.sqlite:sqlite-ktx:2.4.0"
|
||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.4.0"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.0"
|
||||
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.11.1"
|
||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.2.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.4.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.1"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
|
||||
mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0"
|
||||
opusencoder = "io.element.android:opusencoder:1.1.0"
|
||||
|
|
@ -185,10 +185,10 @@ kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
|
|||
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||
|
||||
# Analytics
|
||||
posthog = "com.posthog:posthog-android:3.6.1"
|
||||
posthog = "com.posthog:posthog-android:3.7.3"
|
||||
sentry = "io.sentry:sentry-android:7.14.0"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1"
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
|
||||
|
||||
# Emojibase
|
||||
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ fun Context.copyToClipboard(
|
|||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
*/
|
||||
fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher<Intent>? = null) {
|
||||
fun Context.startNotificationSettingsIntent(
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>? = null,
|
||||
noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found),
|
||||
) {
|
||||
val intent = Intent()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
|
|
@ -85,10 +88,14 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu
|
|||
intent.data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
try {
|
||||
if (activityResultLauncher != null) {
|
||||
activityResultLauncher.launch(intent)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
toast(noActivityFoundMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,13 +86,13 @@ fun ElementLogoAtom(
|
|||
.size(size.logoSize)
|
||||
// Do the same double shadow than on Figma...
|
||||
.shadow(
|
||||
elevation = 25.dp,
|
||||
elevation = 35.dp,
|
||||
clip = false,
|
||||
shape = CircleShape,
|
||||
ambientColor = logoShadowColor,
|
||||
)
|
||||
.shadow(
|
||||
elevation = 25.dp,
|
||||
elevation = 35.dp,
|
||||
clip = false,
|
||||
shape = CircleShape,
|
||||
ambientColor = Color(0x80000000),
|
||||
|
|
|
|||
|
|
@ -7,41 +7,19 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.colors
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.compound.theme.AvatarColors
|
||||
import io.element.android.compound.theme.avatarColorsDark
|
||||
import io.element.android.compound.theme.avatarColorsLight
|
||||
import io.element.android.compound.theme.avatarColors
|
||||
|
||||
object AvatarColorsProvider {
|
||||
private val cache = LruCache<String, AvatarColors>(200)
|
||||
private var currentThemeIsLight = true
|
||||
|
||||
fun provide(id: String, isLightTheme: Boolean): AvatarColors {
|
||||
if (currentThemeIsLight != isLightTheme) {
|
||||
currentThemeIsLight = isLightTheme
|
||||
cache.evictAll()
|
||||
@Composable
|
||||
fun provide(id: String): AvatarColors {
|
||||
return avatarColors().let { colors ->
|
||||
colors[id.toHash(colors.size)]
|
||||
}
|
||||
val valueFromCache = cache.get(id)
|
||||
return if (valueFromCache != null) {
|
||||
valueFromCache
|
||||
} else {
|
||||
val colors = avatarColors(id, isLightTheme)
|
||||
cache.put(id, colors)
|
||||
colors
|
||||
}
|
||||
}
|
||||
|
||||
private fun avatarColors(id: String, isLightTheme: Boolean): AvatarColors {
|
||||
val hash = id.toHash()
|
||||
val colors = if (isLightTheme) {
|
||||
avatarColorsLight[hash]
|
||||
} else {
|
||||
avatarColorsDark[hash]
|
||||
}
|
||||
return colors
|
||||
}
|
||||
}
|
||||
|
||||
internal fun String.toHash(): Int {
|
||||
return toList().sumOf { it.code } % avatarColorsLight.size
|
||||
internal fun String.toHash(maxSize: Int): Int {
|
||||
return toList().sumOf { it.code } % maxSize
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ fun Modifier.avatarBloom(
|
|||
)
|
||||
} else {
|
||||
// There is no URL so we'll generate an avatar with the initials and use that as the bloom source
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
|
||||
val initialsBitmap = initialsBitmap(
|
||||
width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||
height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||
|
|
@ -538,7 +538,7 @@ class InitialsColorIntProvider : PreviewParameterProvider<Int> {
|
|||
@ShowkaseComposable(group = PreviewGroup.Bloom)
|
||||
internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorIntProvider::class) color: Int) {
|
||||
ElementPreview {
|
||||
val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme)
|
||||
val avatarColors = AvatarColorsProvider.provide("$color")
|
||||
val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground)
|
||||
val hash = BlurHash.encode(
|
||||
bitmap = bitmap.asAndroidBitmap(),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.geometry.Offset
|
|||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import timber.log.Timber
|
||||
|
||||
const val LINK_TAG = "URL"
|
||||
|
||||
|
|
@ -65,7 +66,6 @@ fun ClickableLinkText(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun ClickableLinkText(
|
||||
annotatedString: AnnotatedString,
|
||||
|
|
@ -106,14 +106,18 @@ fun ClickableLinkText(
|
|||
) { offset ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
val position = layoutResult.getOffsetForPosition(offset)
|
||||
val linkUrlAnnotations = annotatedString.getUrlAnnotations(position, position)
|
||||
.map { AnnotatedString.Range(it.item.url, it.start, it.end, linkAnnotationTag) }
|
||||
val linkUrlAnnotations = annotatedString.getLinkAnnotations(position, position)
|
||||
.map { AnnotatedString.Range(it.item, it.start, it.end, linkAnnotationTag) }
|
||||
val linkStringAnnotations = linkUrlAnnotations +
|
||||
annotatedString.getStringAnnotations(linkAnnotationTag, position, position)
|
||||
if (linkStringAnnotations.isEmpty()) {
|
||||
onClick()
|
||||
} else {
|
||||
uriHandler.openUri(linkStringAnnotations.first().item)
|
||||
when (val annotation = linkStringAnnotations.first().item) {
|
||||
is LinkAnnotation.Url -> uriHandler.openUri(annotation.url)
|
||||
is String -> uriHandler.openUri(annotation)
|
||||
else -> Timber.e("Unknown link annotation: $annotation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +133,6 @@ fun ClickableLinkText(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTextApi::class)
|
||||
fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
||||
val original = this
|
||||
val spannable = SpannableString(this.text)
|
||||
|
|
@ -141,7 +144,7 @@ fun AnnotatedString.linkify(linkStyle: SpanStyle): AnnotatedString {
|
|||
for (span in spans) {
|
||||
val start = spannable.getSpanStart(span)
|
||||
val end = spannable.getSpanEnd(span)
|
||||
if (original.getUrlAnnotations(start, end).isEmpty() && original.getStringAnnotations("URL", start, end).isEmpty()) {
|
||||
if (original.getLinkAnnotations(start, end).isEmpty() && original.getStringAnnotations("URL", start, end).isEmpty()) {
|
||||
// Prevent linkifying domains in user or room handles (@user:domain.com, #room:domain.com)
|
||||
if (start > 0 && !spannable[start - 1].isWhitespace()) continue
|
||||
addStyle(
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ private fun InitialsAvatar(
|
|||
forcedAvatarSize: Dp?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
|
||||
Box(
|
||||
modifier.background(color = avatarColors.background)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.avatarColorsLight
|
||||
import io.element.android.compound.theme.avatarColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -27,7 +27,7 @@ internal fun UserAvatarColorsPreview() = ElementPreview {
|
|||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
repeat(avatarColorsLight.size) {
|
||||
repeat(avatarColors().size) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -92,7 +92,7 @@ fun GradientFloatingActionButton(
|
|||
enabled = true,
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = Color.White)
|
||||
indication = ripple(color = Color.White)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -45,7 +45,7 @@ fun MainActionButton(
|
|||
enabled: Boolean = true,
|
||||
contentDescription: String = title,
|
||||
) {
|
||||
val ripple = rememberRipple(bounded = false)
|
||||
val ripple = ripple(bounded = false)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Column(
|
||||
modifier
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -111,7 +111,7 @@ fun SuperButton(
|
|||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple()
|
||||
indication = ripple()
|
||||
)
|
||||
.padding(contentPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import androidx.compose.foundation.layout.isImeVisible
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
|
||||
/**
|
||||
* Inspired from https://stackoverflow.com/questions/68847559/how-can-i-detect-keyboard-opening-and-closing-in-jetpack-compose
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.tooltip
|
||||
|
||||
import androidx.compose.material3.CaretScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TooltipScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -19,7 +19,7 @@ import androidx.compose.material3.PlainTooltip as M3PlainTooltip
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CaretScope.PlainTooltip(
|
||||
fun TooltipScope.PlainTooltip(
|
||||
modifier: Modifier = Modifier,
|
||||
contentColor: Color = ElementTheme.colors.textOnSolidPrimary,
|
||||
containerColor: Color = ElementTheme.colors.bgActionPrimaryRest,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.components.tooltip
|
||||
|
||||
import androidx.compose.material3.CaretScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TooltipScope
|
||||
import androidx.compose.material3.TooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -19,7 +19,7 @@ import androidx.compose.material3.TooltipBox as M3TooltipBox
|
|||
@Composable
|
||||
fun TooltipBox(
|
||||
positionProvider: PopupPositionProvider,
|
||||
tooltip: @Composable CaretScope.() -> Unit,
|
||||
tooltip: @Composable TooltipScope.() -> Unit,
|
||||
state: TooltipState,
|
||||
modifier: Modifier = Modifier,
|
||||
focusable: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
package io.element.android.libraries.designsystem.modifiers
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.platform.inspectable
|
||||
|
||||
/**
|
||||
* Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise.
|
||||
|
|
@ -18,15 +16,8 @@ fun Modifier.applyIf(
|
|||
condition: Boolean,
|
||||
ifTrue: Modifier.() -> Modifier,
|
||||
ifFalse: (Modifier.() -> Modifier)? = null
|
||||
): Modifier = this then inspectable(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "applyIf"
|
||||
value = condition
|
||||
}
|
||||
) {
|
||||
this then when {
|
||||
condition -> ifTrue(Modifier)
|
||||
ifFalse != null -> ifFalse(Modifier)
|
||||
else -> Modifier
|
||||
}
|
||||
): Modifier = this then when {
|
||||
condition -> ifTrue(Modifier)
|
||||
ifFalse != null -> ifFalse(Modifier)
|
||||
else -> Modifier
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ fun Modifier.blurredShapeShadow(
|
|||
offsetX: Dp = 0.dp,
|
||||
offsetY: Dp = 0.dp,
|
||||
blurRadius: Dp = 0.dp,
|
||||
) = then(
|
||||
drawBehind {
|
||||
) = drawBehind {
|
||||
drawIntoCanvas { canvas ->
|
||||
val path = Path().apply {
|
||||
addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx())))
|
||||
|
|
@ -78,8 +77,7 @@ fun Modifier.blurredShapeShadow(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun Modifier.blurCompat(
|
||||
radius: Dp,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
||||
fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = then(
|
||||
pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
}
|
||||
)
|
||||
fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = pointerInput(Unit) {
|
||||
detectTapGestures(onTap = {
|
||||
focusManager.clearFocus()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ fun BottomSheetDragHandle(
|
|||
.fillMaxWidth()
|
||||
.requiredHeight(72.dp)
|
||||
.offset(y = 18.dp)
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.border(0.5.dp, ElementTheme.colors.borderDisabled, MaterialTheme.shapes.extraLarge)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fun BottomSheetScaffold(
|
|||
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
|
||||
sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
|
||||
sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
sheetContainerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
sheetContainerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
sheetContentColor: Color = contentColorFor(sheetContainerColor),
|
||||
sheetTonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ package io.element.android.libraries.designsystem.theme.components
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
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.platform.LocalInspectionMode
|
||||
|
|
@ -25,12 +27,14 @@ fun CircularProgressIndicator(
|
|||
progress: () -> Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.circularDeterminateTrackColor,
|
||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth
|
||||
) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = progress,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
}
|
||||
|
|
@ -39,6 +43,7 @@ fun CircularProgressIndicator(
|
|||
fun CircularProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.circularIndeterminateTrackColor,
|
||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||
) {
|
||||
if (LocalInspectionMode.current) {
|
||||
|
|
@ -47,12 +52,14 @@ fun CircularProgressIndicator(
|
|||
modifier = modifier,
|
||||
progress = { 0.75F },
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
} else {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
}
|
||||
|
|
@ -61,12 +68,18 @@ fun CircularProgressIndicator(
|
|||
@Preview(group = PreviewGroup.Progress)
|
||||
@Composable
|
||||
internal fun CircularProgressIndicatorPreview() = ElementThemedPreview(vertical = false) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Column(
|
||||
modifier = Modifier.padding(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Indeterminate progress
|
||||
Text("Indeterminate")
|
||||
CircularProgressIndicator()
|
||||
// Fixed progress
|
||||
Text("Fixed progress")
|
||||
CircularProgressIndicator(
|
||||
progress = { 0.90F }
|
||||
progress = { 0.50F }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
|
|
@ -42,12 +43,12 @@ fun ModalBottomSheet(
|
|||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
tonalElevation: Dp = if (ElementTheme.isLightTheme) 0.dp else BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState
|
||||
|
|
@ -61,7 +62,7 @@ fun ModalBottomSheet(
|
|||
tonalElevation = tonalElevation,
|
||||
scrimColor = scrimColor,
|
||||
dragHandle = dragHandle,
|
||||
windowInsets = windowInsets,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarColors
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
|
@ -56,8 +57,10 @@ fun <T> SearchBar(
|
|||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
inactiveColors: SearchBarColors = ElementSearchBarDefaults.inactiveColors(),
|
||||
activeColors: SearchBarColors = ElementSearchBarDefaults.activeColors(),
|
||||
inactiveBarColors: SearchBarColors = ElementSearchBarDefaults.inactiveColors(),
|
||||
activeBarColors: SearchBarColors = ElementSearchBarDefaults.activeColors(),
|
||||
inactiveTextInputColors: TextFieldColors = ElementSearchBarDefaults.inactiveInputFieldColors(),
|
||||
activeTextInputColors: TextFieldColors = ElementSearchBarDefaults.activeInputFieldColors(),
|
||||
contentPrefix: @Composable ColumnScope.() -> Unit = {},
|
||||
contentSuffix: @Composable ColumnScope.() -> Unit = {},
|
||||
resultHandler: @Composable ColumnScope.(T) -> Unit = {},
|
||||
|
|
@ -69,51 +72,58 @@ fun <T> SearchBar(
|
|||
focusManager.clearFocus()
|
||||
}
|
||||
|
||||
androidx.compose.material3.SearchBar(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
active = active,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||
enabled = enabled,
|
||||
placeholder = {
|
||||
Text(text = placeHolderTitle)
|
||||
},
|
||||
leadingIcon = if (showBackButton && active) {
|
||||
{ BackButton(onClick = { onActiveChange(false) }) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
trailingIcon = when {
|
||||
active && query.isNotEmpty() -> {
|
||||
{
|
||||
IconButton(onClick = { onQueryChange("") }) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_clear),
|
||||
)
|
||||
SearchBar(
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
expanded = active,
|
||||
onExpandedChange = onActiveChange,
|
||||
enabled = enabled,
|
||||
placeholder = {
|
||||
Text(text = placeHolderTitle)
|
||||
},
|
||||
leadingIcon = if (showBackButton && active) {
|
||||
{ BackButton(onClick = { onActiveChange(false) }) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
trailingIcon = when {
|
||||
active && query.isNotEmpty() -> {
|
||||
{
|
||||
IconButton(onClick = { onQueryChange("") }) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_clear),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
!active -> {
|
||||
{
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Search(),
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
!active -> {
|
||||
{
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Search(),
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
tint = ElementTheme.materialColors.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
else -> null
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
colors = if (active) activeTextInputColors else inactiveTextInputColors,
|
||||
)
|
||||
},
|
||||
expanded = active,
|
||||
onExpandedChange = onActiveChange,
|
||||
modifier = modifier.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||
shape = shape,
|
||||
colors = if (active) activeColors else inactiveColors,
|
||||
colors = if (active) activeBarColors else inactiveBarColors,
|
||||
tonalElevation = tonalElevation,
|
||||
windowInsets = windowInsets,
|
||||
interactionSource = interactionSource,
|
||||
content = {
|
||||
contentPrefix()
|
||||
when (resultState) {
|
||||
|
|
@ -128,7 +138,7 @@ fun <T> SearchBar(
|
|||
Text(
|
||||
text = stringResource(CommonStrings.common_no_results),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
color = ElementTheme.materialColors.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
|
@ -147,28 +157,34 @@ object ElementSearchBarDefaults {
|
|||
@Composable
|
||||
fun inactiveColors() = SearchBarDefaults.colors(
|
||||
containerColor = ElementTheme.materialColors.surfaceVariant,
|
||||
inputFieldColors = TextFieldDefaults.colors(
|
||||
unfocusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
unfocusedLeadingIconColor = MaterialTheme.colorScheme.primary,
|
||||
focusedLeadingIconColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedTrailingIconColor = MaterialTheme.colorScheme.primary,
|
||||
focusedTrailingIconColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
dividerColor = ElementTheme.materialColors.outline,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun inactiveInputFieldColors() = TextFieldDefaults.colors(
|
||||
unfocusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
unfocusedLeadingIconColor = ElementTheme.materialColors.primary,
|
||||
focusedLeadingIconColor = ElementTheme.materialColors.primary,
|
||||
unfocusedTrailingIconColor = ElementTheme.materialColors.primary,
|
||||
focusedTrailingIconColor = ElementTheme.materialColors.primary,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun activeColors() = SearchBarDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
inputFieldColors = TextFieldDefaults.colors(
|
||||
unfocusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
unfocusedLeadingIconColor = MaterialTheme.colorScheme.primary,
|
||||
focusedLeadingIconColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedTrailingIconColor = MaterialTheme.colorScheme.primary,
|
||||
focusedTrailingIconColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
dividerColor = ElementTheme.materialColors.outline,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun activeInputFieldColors() = TextFieldDefaults.colors(
|
||||
unfocusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
focusedPlaceholderColor = ElementTheme.colors.textDisabled,
|
||||
unfocusedLeadingIconColor = ElementTheme.materialColors.primary,
|
||||
focusedLeadingIconColor = ElementTheme.materialColors.primary,
|
||||
unfocusedTrailingIconColor = ElementTheme.materialColors.primary,
|
||||
focusedTrailingIconColor = ElementTheme.materialColors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
|||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -64,7 +65,12 @@ fun TextField(
|
|||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
|
||||
focusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
|
||||
disabledContainerColor = ElementTheme.colors.bgSubtleSecondary,
|
||||
errorContainerColor = ElementTheme.colors.bgSubtleSecondary,
|
||||
)
|
||||
) {
|
||||
androidx.compose.material3.TextField(
|
||||
value = value,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fun CustomBottomSheetScaffold(
|
|||
sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
|
||||
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
|
||||
sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
sheetContainerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
sheetContainerColor: Color = Color.White,
|
||||
sheetContentColor: Color = contentColorFor(sheetContainerColor),
|
||||
sheetTonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
|
|
@ -367,6 +367,12 @@ private class MapDraggableAnchors<T>(private val anchors: Map<T, Float>) : Dragg
|
|||
return anchors == other.anchors
|
||||
}
|
||||
|
||||
override fun forEach(block: (anchor: T, position: Float) -> Unit) {
|
||||
for (anchor in anchors) {
|
||||
block(anchor.key, anchor.value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = 31 * anchors.hashCode()
|
||||
|
||||
override fun toString() = "MapDraggableAnchors($anchors)"
|
||||
|
|
@ -381,7 +387,7 @@ internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
|||
): NestedScrollConnection = object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.Drag) {
|
||||
return if (delta < 0 && source == NestedScrollSource.UserInput) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
|
|
@ -393,7 +399,7 @@ internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
|||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
return if (source == NestedScrollSource.Drag) {
|
||||
return if (source == NestedScrollSource.UserInput) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.theme.components.bottomsheet
|
||||
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
|
|
@ -201,14 +203,12 @@ constructor(
|
|||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
* @param velocity The velocity of the animation
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal suspend fun animateTo(
|
||||
targetValue: SheetValue,
|
||||
velocity: Float = anchoredDraggableState.lastVelocity
|
||||
) {
|
||||
anchoredDraggableState.animateTo(targetValue, velocity)
|
||||
anchoredDraggableState.animateTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -235,7 +235,8 @@ constructor(
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
animationSpec = AnchoredDraggableDefaults.AnimationSpec,
|
||||
snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec,
|
||||
decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec,
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } }
|
||||
|
|
@ -298,5 +299,10 @@ internal object AnchoredDraggableDefaults {
|
|||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val AnimationSpec = SpringSpec<Float>()
|
||||
val SnapAnimationSpec = SpringSpec<Float>()
|
||||
|
||||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val DecayAnimationSpec = exponentialDecay<Float>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ package io.element.android.libraries.designsystem.utils
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
|
||||
@Composable
|
||||
fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) {
|
||||
|
|
|
|||
|
|
@ -8,32 +8,26 @@
|
|||
package io.element.android.libraries.designsystem.colors
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.compound.theme.avatarColorsDark
|
||||
import io.element.android.compound.theme.avatarColorsLight
|
||||
import org.junit.Test
|
||||
|
||||
class AvatarColorsTest {
|
||||
@Test
|
||||
fun `ensure the size of the avatar color are equal for light and dark theme`() {
|
||||
assertThat(avatarColorsDark.size).isEqualTo(avatarColorsLight.size)
|
||||
}
|
||||
|
||||
private val maxSize = 6
|
||||
@Test
|
||||
fun `compute string hash`() {
|
||||
assertThat("@alice:domain.org".toHash()).isEqualTo(6)
|
||||
assertThat("@bob:domain.org".toHash()).isEqualTo(3)
|
||||
assertThat("@charlie:domain.org".toHash()).isEqualTo(0)
|
||||
assertThat("@alice:domain.org".toHash(maxSize)).isEqualTo(0)
|
||||
assertThat("@bob:domain.org".toHash(maxSize)).isEqualTo(1)
|
||||
assertThat("@charlie:domain.org".toHash(maxSize)).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compute string hash reverse`() {
|
||||
assertThat("0".toHash()).isEqualTo(0)
|
||||
assertThat("1".toHash()).isEqualTo(1)
|
||||
assertThat("2".toHash()).isEqualTo(2)
|
||||
assertThat("3".toHash()).isEqualTo(3)
|
||||
assertThat("4".toHash()).isEqualTo(4)
|
||||
assertThat("5".toHash()).isEqualTo(5)
|
||||
assertThat("6".toHash()).isEqualTo(6)
|
||||
assertThat("7".toHash()).isEqualTo(7)
|
||||
assertThat("0".toHash(maxSize)).isEqualTo(0)
|
||||
assertThat("1".toHash(maxSize)).isEqualTo(1)
|
||||
assertThat("2".toHash(maxSize)).isEqualTo(2)
|
||||
assertThat("3".toHash(maxSize)).isEqualTo(3)
|
||||
assertThat("4".toHash(maxSize)).isEqualTo(4)
|
||||
assertThat("5".toHash(maxSize)).isEqualTo(5)
|
||||
assertThat("6".toHash(maxSize)).isEqualTo(0)
|
||||
assertThat("7".toHash(maxSize)).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,9 +123,9 @@ enum class FeatureFlags(
|
|||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
InvisibleCrypto(
|
||||
key = "feature.invisibleCrypto",
|
||||
title = "Invisible Crypto",
|
||||
OnlySignedDeviceIsolationMode(
|
||||
key = "feature.onlySignedDeviceIsolationMode",
|
||||
title = "Exclude not secure devices when sending/receiving messages",
|
||||
description = "This setting controls how end-to-end encryption (E2E) keys are shared." +
|
||||
" Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners." +
|
||||
" You'll have to stop and re-open the app manually for that setting to take effect.",
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ sealed interface NotificationContent {
|
|||
data object RoomHistoryVisibility : StateEvent
|
||||
data object RoomJoinRules : StateEvent
|
||||
data class RoomMemberContent(
|
||||
val userId: String,
|
||||
val userId: UserId,
|
||||
val membershipState: RoomMembershipState
|
||||
) : StateEvent
|
||||
|
||||
|
|
@ -108,6 +108,10 @@ sealed interface NotificationContent {
|
|||
data object SpaceChild : StateEvent
|
||||
data object SpaceParent : StateEvent
|
||||
}
|
||||
|
||||
data class Invite(
|
||||
val senderId: UserId,
|
||||
) : NotificationContent
|
||||
}
|
||||
|
||||
enum class CallNotifyType {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ package io.element.android.libraries.matrix.api.notification
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface NotificationService {
|
||||
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue