Merge branch 'develop' into feature/fga/live_location_sharing_setup

This commit is contained in:
ganfra 2026-03-12 12:48:55 +01:00
commit e8c2790595
131 changed files with 825 additions and 407 deletions

View file

@ -1,14 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
"config:recommended",
],
"labels": [
"PR-Dependencies"
"PR-Dependencies",
],
"ignoreDeps": [
"string:app_name",
"gradle"
"gradle",
],
"packageRules": [
{
@ -16,15 +16,25 @@
"matchPackageNames": [
"/^org.jetbrains.kotlin/",
"/^com.google.devtools.ksp/",
"/^androidx.compose.compiler/"
]
"/^androidx.compose.compiler/",
],
},
{
"versioning": "semver",
"matchPackageNames": [
"/^org.maplibre/",
"/^org.jetbrains.kotlinx:kotlinx-datetime/"
]
}
"/^org.jetbrains.kotlinx:kotlinx-datetime/",
],
},
{
// Limit PostHog Android upgrade to one PR per month, the first day of the month
"matchPackageNames": [
"com.posthog:posthog-android",
],
"schedule": [
"* * 1 * *",
]
},
],
}

View file

@ -86,7 +86,7 @@ jobs:
ref: ${{ github.ref }}
persist-credentials: false
- name: Download APK artifact from previous job
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: elementx-apk-maestro
- name: Enable KVM group perms
@ -98,7 +98,7 @@ jobs:
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: Run Maestro tests in emulator
id: maestro_test
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
uses: reactivecircus/android-emulator-runner@5d6e86df22ab11632167a1a6b0c9ab0dc3469586 # v2.36.0
continue-on-error: true
env:
MAESTRO_USERNAME: maestroelement

View file

@ -287,12 +287,12 @@ jobs:
path: |
**/build/reports/**/*.*
knit:
name: Knit checks
docs:
name: Doc checks
runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR.
concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-knit-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-knit-develop-{0}', github.sha) || format('check-knit-{0}', github.ref) }}
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@ -309,17 +309,9 @@ jobs:
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Knit
run: ./gradlew knitCheck $CI_GRADLE_ARG_PROPERTIES
- name: Run docs check
# This is equivalent to `./gradlew checkDocs`, but we avoid having to install java and gradle
run: python3 ./tools/docs/generate_toc.py --verify ./*.md docs/**/*.md
# Note: to auto fix issues you can use the following command:
# shellcheck -f diff <files> | git apply
@ -359,7 +351,7 @@ jobs:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Download reports from previous jobs
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Prepare Danger
if: always()
run: |

View file

@ -1,3 +1,36 @@
Changes in Element X v26.03.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.03.3 -->
## What's Changed
### ✨ Features
* Support for Voice Call only (no video), parity with web by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/5995
### 🐛 Bugfixes
* Fix read receipts not appearing in threaded timelines by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6297
* Try fixing index OOB issues in `Editable.checkSuggestionNeeded` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6303
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6302
### 🧱 Build
* Add zizmorcore/zizmor-action by @bmarty in https://github.com/element-hq/element-x-android/pull/6286
* Add use existing branch confirmation and progress for file download by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6294
* Replace `knit` with `generate_toc.py` script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6279
### Dependency upgrades
* Update plugin sonarqube to v7.2.3.7755 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6283
* Update dependency io.sentry:sentry-android to v8.34.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6289
* Update dependency org.matrix.rustcomponents:sdk-android to v26.03.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6292
* Update dependency com.posthog:posthog-android to v3.35.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6293
* Update zizmorcore/zizmor-action action to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6299
* fix(deps): update dependency org.maplibre.gl:android-sdk to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6277
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.09 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6307
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6310
### Others
* Add code to help debugging the saved nav state graph by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6295
* Add network constraints for fetching notifications with `WorkManager` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6305
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.2...v26.03.3
Changes in Element X v26.03.2
=============================

View file

@ -16,7 +16,7 @@
* [Code quality](#code-quality)
* [detekt](#detekt)
* [ktlint](#ktlint)
* [knit](#knit)
* [checkDocs](#checkdocs)
* [lint](#lint)
* [Unit tests](#unit-tests)
* [konsist](#konsist)
@ -123,13 +123,13 @@ Note that you can run
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
#### knit
#### checkDocs
[knit](https://github.com/Kotlin/kotlinx-knit) is a tool which checks markdown files on the project. Also it generates/updates the table of content (toc) of the markdown files.
`checkDocs` is a Gradle task which checks markdown files on the project to ensure their table of contents is up to date. It uses `tools/docs/generate_toc.py --verify` under the hood, and has a counterpart `generateDocsToc` task which runs `tools/docs/generate_toc.py` to update the table of contents of markdown files.
So everytime the toc should be updated, just run
<pre>
./gradlew knit
./gradlew generateDocsToc
</pre>
and commit the changes.
@ -137,7 +137,7 @@ and commit the changes.
The CI will check that markdown files are up to date by running
<pre>
./gradlew knitCheck
./gradlew checkDocs
</pre>
#### lint

View file

@ -33,7 +33,6 @@ plugins {
alias(libs.plugins.kotlin.android)
// When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit)
id("kotlin-parcelize")
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
@ -250,26 +249,6 @@ androidComponents {
configureLicensesTasks(reportingExtension)
}
// Knit
apply {
plugin("kotlinx-knit")
}
knit {
files = fileTree(project.rootDir) {
include(
"**/*.md",
"**/*.kt",
"*/*.kts",
)
exclude(
"**/build/**",
"*/.gradle/**",
"**/CHANGES.md",
)
}
}
setupDependencyInjection()
dependencies {

View file

@ -175,12 +175,23 @@ tasks.register("runQualityChecks") {
tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
// tasks.findByName("buildHealth")?.let { dependsOn(it) }
}
dependsOn(":app:knitCheck")
dependsOn("checkDocs")
// Make sure all checks run even if some fail
gradle.startParameter.isContinueOnFailure = true
}
// Register Markdown documentation check task.
tasks.register("checkDocs", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", "--verify", *inputs.files.map { it.path }.toTypedArray())
}
// Register Markdown documentation TOC generation task.
tasks.register("generateDocsToc", Exec::class.java) {
inputs.files("./*.md", "docs/**/*.md")
commandLine("python3", "tools/docs/generate_toc.py", *inputs.files.map { it.path }.toTypedArray())
}
// Make sure to delete old screenshots before recording new ones
subprojects {
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")

View file

@ -10,8 +10,8 @@ This document explains how to install Element X Android from a Github Release.
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
* [Installing from the App Bundle](#installing-from-the-app-bundle)
* [Requirements](#requirements)
* [Steps](#steps)
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone)
* [Steps](#steps-1)
* [I already have the application on my phone](#i-already-have-the-application-on-my-phone-1)
<!--- END -->

View file

@ -2,11 +2,11 @@
<!--- TOC -->
* [Installing from GitHub](#installing-from-github)
* [Installing from GitHub](#installing-from-github)
* [Create a GitHub token](#create-a-github-token)
* [Provide artifact URL](#provide-artifact-url)
* [Next steps](#next-steps)
* [Future improvement](#future-improvement)
* [Provide artifact URL](#provide-artifact-url)
* [Next steps](#next-steps)
* [Future improvement](#future-improvement)
<!--- END -->

View file

@ -8,7 +8,7 @@
* [Stop Synapse](#stop-synapse)
* [Troubleshoot](#troubleshoot)
* [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver)
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost8080")
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-unable-to-contact-localhost8080)
* [virtualenv command fails](#virtualenv-command-fails)
<!--- END -->

View file

@ -5,11 +5,11 @@ This document aims to describe how Element android displays notifications to the
<!--- TOC -->
* [Prerequisites Knowledge](#prerequisites-knowledge)
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver?)
* [How does a matrix client get a message from a homeserver?](#how-does-a-matrix-client-get-a-message-from-a-homeserver)
* [How does a mobile app receives push notification](#how-does-a-mobile-app-receives-push-notification)
* [Push VS Notification](#push-vs-notification)
* [Push in the matrix federated world](#push-in-the-matrix-federated-world)
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client?)
* [How does the homeserver know when to notify a client?](#how-does-the-homeserver-know-when-to-notify-a-client)
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
* [Background processing limitations](#background-processing-limitations)
* [Element Notification implementations](#element-notification-implementations)

View file

@ -3,23 +3,23 @@
<!--- TOC -->
* [Introduction](#introduction)
* [Who should read this document?](#who-should-read-this-document?)
* [Who should read this document?](#who-should-read-this-document)
* [Submitting PR](#submitting-pr)
* [Who can submit pull requests?](#who-can-submit-pull-requests?)
* [Who can submit pull requests?](#who-can-submit-pull-requests)
* [Humans](#humans)
* [Draft PR?](#draft-pr?)
* [Draft PR?](#draft-pr)
* [Base branch](#base-branch)
* [PR Review Assignment](#pr-review-assignment)
* [PR review time](#pr-review-time)
* [Re-request PR review](#re-request-pr-review)
* [When create split PR?](#when-create-split-pr?)
* [When create split PR?](#when-create-split-pr)
* [Avoid fixing other unrelated issue in a big PR](#avoid-fixing-other-unrelated-issue-in-a-big-pr)
* [Bots](#bots)
* [Renovate](#renovate)
* [Gradle wrapper](#gradle-wrapper)
* [Sync analytics plan](#sync-analytics-plan)
* [Reviewing PR](#reviewing-pr)
* [Who can review pull requests?](#who-can-review-pull-requests?)
* [Who can review pull requests?](#who-can-review-pull-requests)
* [What to have in mind when reviewing a PR](#what-to-have-in-mind-when-reviewing-a-pr)
* [Rules](#rules)
* [Check the form](#check-the-form)
@ -29,7 +29,7 @@
* [Check the commit](#check-the-commit)
* [Check the substance](#check-the-substance)
* [Make a dedicated meeting to review the PR](#make-a-dedicated-meeting-to-review-the-pr)
* [What happen to the issue(s)?](#what-happen-to-the-issues?)
* [What happen to the issue(s)?](#what-happen-to-the-issues)
* [Merge conflict](#merge-conflict)
* [When and who can merge PR](#when-and-who-can-merge-pr)
* [Merge type](#merge-type)

@ -1 +1 @@
Subproject commit 1fd0d297d944186e3af2773e1c5db2938d60f74b
Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and improvements.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -24,5 +24,11 @@ interface NetworkMonitor {
/**
* Checks if the active network is being blocked by Doze, even if it's available.
*/
fun isNetworkBlocked(): Boolean
val isNetworkBlocked: StateFlow<Boolean>
/**
* A flow indicating whether the app is running in an air-gapped environment.
* An air-gapped environment is an environment that is not connected to the internet, and where the app can only communicate with a limited set of servers.
*/
val isInAirGappedEnvironment: StateFlow<Boolean>
}

View file

@ -1,4 +1,5 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
@ -23,4 +24,8 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.features.networkmonitor.api)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.networkmonitor.test)
}

View file

@ -13,18 +13,21 @@ package io.element.android.features.networkmonitor.impl
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@ -39,13 +42,13 @@ import java.util.concurrent.atomic.AtomicInteger
@SingleIn(AppScope::class)
class DefaultNetworkMonitor(
@ApplicationContext context: Context,
@AppCoroutineScope
appCoroutineScope: CoroutineScope,
@AppCoroutineScope appCoroutineScope: CoroutineScope,
private val buildMeta: BuildMeta,
) : NetworkMonitor {
private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java)
private val blockedNetworkBlockedChecker = NetworkBlockedChecker(connectivityManager)
override fun isNetworkBlocked(): Boolean = blockedNetworkBlockedChecker.isNetworkBlocked()
override val isNetworkBlocked = MutableStateFlow(NetworkBlockedChecker(connectivityManager).isNetworkBlocked())
override val isInAirGappedEnvironment = MutableStateFlow(false)
override val connectivity: StateFlow<NetworkStatus> = callbackFlow {
@ -63,6 +66,27 @@ class DefaultNetworkMonitor(
}
}
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Timber.d("Network ${network.networkHandle} blocked status changed: $blocked.")
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
// If the network is blocked, it means that Doze is preventing the app from using the network, even if it's available.
isNetworkBlocked.value = blocked
}
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
if (!buildMeta.isEnterpriseBuild) {
// The air-gapped environment detection is only relevant for the enterprise build.
return
}
if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) {
// If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet
// (according to Google), which is a common case in air-gapped environments.
isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
}
override fun onAvailable(network: Network) {
if (activeNetworksCount.incrementAndGet() > 0) {
trySendBlocking(NetworkStatus.Connected)

View file

@ -14,10 +14,10 @@ import android.net.ConnectivityManager
import android.net.NetworkInfo
/**
* Helper to check if the active network in [ConnectivityManager] is blocked.
* Helper to synchronously check if the active network in [ConnectivityManager] is blocked.
*
* This is extracted to its own class because it uses deprecated APIs (but the only ones that are reliable)
* and we don't want to suppress deprecations everywhere.
* and we don't want to suppress deprecations everywhere in the file this would be called.
*/
class NetworkBlockedChecker(
private val connectivityManager: ConnectivityManager,

View file

@ -14,8 +14,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
class FakeNetworkMonitor(
initialStatus: NetworkStatus = NetworkStatus.Connected,
private val isNetworkBlockedLambda: () -> Boolean = { false },
) : NetworkMonitor {
override val connectivity = MutableStateFlow(initialStatus)
override fun isNetworkBlocked(): Boolean = isNetworkBlockedLambda()
override val isNetworkBlocked = MutableStateFlow(false)
override val isInAirGappedEnvironment = MutableStateFlow(false)
fun givenNetworkBlocked(isBlocked: Boolean) {
isNetworkBlocked.value = isBlocked
}
fun givenIsInAirGappedEnvironment(isInAirGapped: Boolean) {
isInAirGappedEnvironment.value = isInAirGapped
}
}

View file

@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.6"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.11"
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@ -220,7 +220,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics
posthog = "com.posthog:posthog-android:3.35.0"
posthog = "com.posthog:posthog-android:3.37.0"
sentry = "io.sentry:sentry-android:8.34.1"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
@ -267,7 +267,6 @@ paparazzi = "app.cash.paparazzi:2.0.0-alpha04"
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.1" }
sonarqube = "org.sonarqube:7.2.3.7755"
licensee = "app.cash.licensee:1.14.1"
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View file

@ -154,4 +154,12 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
ValidateNetworkWhenSchedulingNotificationFetching(
key = "feature.validate_network_when_scheduling_notification_fetching",
title = "validate internet connectivity when scheduling notification fetching",
description = "Only fetch events for push notifications when the device has internet connectivity. " +
"Enabling this can be problematic in air-gapped environments.",
defaultValue = { true },
isFinished = false,
),
}

View file

@ -214,7 +214,7 @@ class RustEncryptionService(
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
runCatchingExceptions {
service.recover(recoveryKey)
service.recoverAndFixBackup(recoveryKey)
}.recoverCatching {
when (it) {
// We ignore import errors because the user will be notified about them via the "Key storage out of sync" detection.

View file

@ -324,7 +324,7 @@ class JoinedRustRoom(
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
runCatchingExceptions {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
innerRoom.reportContent(eventId = eventId.value, reason = reason)
if (blockUserId != null) {
innerRoom.ignoreUser(blockUserId.value)
}

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.flow.first
import timber.log.Timber
@ -49,7 +48,7 @@ class DefaultPushHandler(
private val analyticsService: AnalyticsService,
private val systemClock: SystemClock,
private val workManagerScheduler: WorkManagerScheduler,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory,
resultProcessor: NotificationResultProcessor,
) : PushHandler {
init {
@ -134,12 +133,7 @@ class DefaultPushHandler(
if (!workManagerScheduler.hasPendingWork(userId, WorkManagerRequestType.NOTIFICATION_SYNC)) {
Timber.d("No pending worker for push notifications found")
workManagerScheduler.submit(
SyncPendingNotificationsRequestBuilder(
sessionId = userId,
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
)
)
workManagerScheduler.submit(syncPendingNotificationsRequestFactory.create(userId))
}
} catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")

View file

@ -160,7 +160,8 @@ class FetchPendingNotificationsWorker(
networkTimeoutSpans.finish()
// If there is a problem with the updated network values, report it and retry if needed
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = networkMonitor.isNetworkBlocked())) {
val isNetworkBlocked = networkMonitor.isNetworkBlocked.first()
if (reportConnectivityError(requests = requests, hasNetwork = hasNetwork, isNetworkBlocked = isNetworkBlocked)) {
pushHistoryService.insertOrUpdatePushRequests(requests.map { request ->
request.copy(retries = request.retries + 1)
})

View file

@ -8,32 +8,87 @@
package io.element.android.libraries.push.impl.workmanager
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.workDataOf
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder.Companion.SESSION_ID
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
import io.element.android.libraries.workmanager.api.workManagerTag
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.flow.first
import timber.log.Timber
interface SyncPendingNotificationsRequestBuilder : WorkManagerRequestBuilder {
fun interface Factory {
fun create(sessionId: SessionId): SyncPendingNotificationsRequestBuilder
}
class SyncPendingNotificationsRequestBuilder(
private val sessionId: SessionId,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : WorkManagerRequestBuilder {
companion object {
const val SESSION_ID = "session_id"
}
}
@AssistedInject
class DefaultSyncPendingNotificationsRequestBuilder(
@Assisted private val sessionId: SessionId,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val networkMonitor: NetworkMonitor,
private val featureFlagService: FeatureFlagService,
) : SyncPendingNotificationsRequestBuilder {
@AssistedFactory
@ContributesBinding(AppScope::class)
interface Factory : SyncPendingNotificationsRequestBuilder.Factory {
override fun create(sessionId: SessionId): DefaultSyncPendingNotificationsRequestBuilder
}
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> {
val type = WorkManagerWorkerType.Unique(
name = workManagerTag(sessionId = sessionId, requestType = WorkManagerRequestType.NOTIFICATION_SYNC),
policy = ExistingWorkPolicy.APPEND_OR_REPLACE,
)
val networkRequestBuilder = NetworkRequest.Builder()
// Allow any kind of network that can have internet connectivity.
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
// By default, the network request will require the device to not be in VPN, but since some customers use a VPN to connect to their homeserver,
// we need to allow VPN networks.
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
// If we're in an air-gapped environment, we shouldn't validate internet connectivity, as the checker will fail and the worker won't run at all.
// Note this will always be false for FOSS, since the feature is only enabled in Element Pro.
if (networkMonitor.isInAirGappedEnvironment.first()) {
Timber.d("In an air-gapped environment, not adding NET_CAPABILITY_VALIDATED to the network request")
networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else if (featureFlagService.isFeatureEnabled(FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching)) {
Timber.d("Not in an air-gapped environment, adding NET_CAPABILITY_VALIDATED to the network request")
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
val networkConstraints = Constraints.Builder()
.setRequiredNetworkRequest(networkRequestBuilder.build(), NetworkType.NOT_REQUIRED)
.build()
val request = OneTimeWorkRequestBuilder<FetchPendingNotificationsWorker>()
.setInputData(workDataOf(SESSION_ID to sessionId.value))
.apply {
@ -44,8 +99,10 @@ class SyncPendingNotificationsRequestBuilder(
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
}
}
.setConstraints(networkConstraints)
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
.build()
return Result.success(listOf(WorkManagerRequestWrapper(request, type)))
}
}

View file

@ -26,6 +26,8 @@ import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.notifications.FakeNotificationResultProcessor
import io.element.android.libraries.push.impl.test.DefaultTestPush
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder
import io.element.android.libraries.push.test.workmanager.FakeSyncPendingNotificationsRequestBuilder
import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore
@ -34,7 +36,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa
import io.element.android.libraries.workmanager.api.WorkManagerRequestBuilder
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
@ -216,7 +217,6 @@ class DefaultPushHandlerTest {
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
systemClock: FakeSystemClock = FakeSystemClock(),
buildVersionSdkIntProvider: FakeBuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
resultProcessor: FakeNotificationResultProcessor = FakeNotificationResultProcessor(
emit = { Result.success(Unit) },
start = {},
@ -238,8 +238,10 @@ class DefaultPushHandlerTest {
analyticsService = analyticsService,
systemClock = systemClock,
workManagerScheduler = workManagerScheduler,
buildVersionSdkIntProvider = buildVersionSdkIntProvider,
resultProcessor = resultProcessor,
syncPendingNotificationsRequestFactory = SyncPendingNotificationsRequestBuilder.Factory {
FakeSyncPendingNotificationsRequestBuilder()
}
)
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.workmanager
import android.net.NetworkCapabilities
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.OneTimeWorkRequest
import androidx.work.hasKeyWithValueOfType
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
import io.element.android.libraries.workmanager.api.workManagerTag
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DefaultSyncPendingNotificationsRequestBuilderTest {
@Test
fun `build - success API 33`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 33,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
result.request.run {
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
assertThat(workSpec.hasConstraints()).isTrue()
// True in API 33+
assertThat(workSpec.expedited).isTrue()
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
}
}
}
@Test
fun `build - success API 32 and lower`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 32,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
result.request.run {
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
assertThat(workSpec.hasConstraints()).isTrue()
// False before API 33
assertThat(workSpec.expedited).isFalse()
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
}
}
}
@Test
fun `build - has NET_CAPABILITY_VALIDATED constraint if not in air-gapped env`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 33,
isInAirGapEnvironment = false,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
result.request.run {
assertThat(workSpec.hasConstraints()).isTrue()
val networkRequest = workSpec.constraints.requiredNetworkRequest
assertThat(networkRequest).isNotNull()
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isTrue()
}
}
}
@Test
fun `build - does not have NET_CAPABILITY_VALIDATED constraint if in air-gapped env`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 33,
isInAirGapEnvironment = true,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
result.request.run {
assertThat(workSpec.hasConstraints()).isTrue()
val networkRequest = workSpec.constraints.requiredNetworkRequest
assertThat(networkRequest).isNotNull()
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isFalse()
}
}
}
@Test
fun `build - does not have NET_CAPABILITY_VALIDATED constraint if feature flag is disabled`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 33,
isInAirGapEnvironment = false,
featureFlagService = FakeFeatureFlagService(initialState = mapOf(
FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to false
)),
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
result.request.run {
assertThat(workSpec.hasConstraints()).isTrue()
val networkRequest = workSpec.constraints.requiredNetworkRequest
assertThat(networkRequest).isNotNull()
assertThat(networkRequest!!.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED)).isFalse()
}
}
}
}
private fun createSyncPendingNotificationsRequestBuilder(
sessionId: SessionId,
sdkVersion: Int = 33,
isInAirGapEnvironment: Boolean = false,
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
) = DefaultSyncPendingNotificationsRequestBuilder(
sessionId = sessionId,
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkVersion),
networkMonitor = FakeNetworkMonitor().apply { givenIsInAirGappedEnvironment(isInAirGapEnvironment) },
featureFlagService = featureFlagService,
)

View file

@ -1,74 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.impl.workmanager
import androidx.work.OneTimeWorkRequest
import androidx.work.hasKeyWithValueOfType
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.workmanager.api.WorkManagerRequestType
import io.element.android.libraries.workmanager.api.WorkManagerWorkerType
import io.element.android.libraries.workmanager.api.workManagerTag
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
class SyncPendingNotificationsRequestBuilderTest {
@Test
fun `build - success API 33`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 33,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
result.request.run {
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
// True in API 33+
assertThat(workSpec.expedited).isTrue()
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
}
}
}
@Test
fun `build - success API 32 and lower`() = runTest {
val request = createSyncPendingNotificationsRequestBuilder(
sessionId = A_SESSION_ID,
sdkVersion = 32,
)
val results = request.build()
assertThat(results.isSuccess).isTrue()
results.getOrNull()!!.first().let { result ->
assertThat(result.type).isInstanceOf(WorkManagerWorkerType.Unique::class.java)
result.request.run {
assertThat(this).isInstanceOf(OneTimeWorkRequest::class.java)
assertThat(workSpec.input.hasKeyWithValueOfType<String>(SyncPendingNotificationsRequestBuilder.SESSION_ID)).isTrue()
// False before API 33
assertThat(workSpec.expedited).isFalse()
assertThat(workSpec.traceTag).isEqualTo(workManagerTag(A_SESSION_ID, WorkManagerRequestType.NOTIFICATION_SYNC))
}
}
}
}
private fun createSyncPendingNotificationsRequestBuilder(
sessionId: SessionId,
sdkVersion: Int = 33,
) = SyncPendingNotificationsRequestBuilder(
sessionId = sessionId,
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkVersion),
)

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.libraries.push.impl)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.workmanager.api)
implementation(projects.tests.testutils)
implementation(libs.androidx.core)
implementation(libs.coil.compose)

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.push.test.workmanager
import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder
import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper
class FakeSyncPendingNotificationsRequestBuilder(
private val build: () -> Result<List<WorkManagerRequestWrapper>> = { Result.success(emptyList()) },
) : SyncPendingNotificationsRequestBuilder {
override suspend fun build(): Result<List<WorkManagerRequestWrapper>> = build.invoke()
}

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.textcomposer.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -17,14 +18,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.colors.gradientActionColors
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
@ -33,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
/**
* Send button for the message composer.
* Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1956-37575&node-type=frame&m=dev
* Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev
*/
@Composable
internal fun SendButtonIcon(
@ -49,11 +45,16 @@ internal fun SendButtonIcon(
isEditing -> 0.dp
else -> 2.dp
}
val backgroundColor = if (canSendMessage) {
ElementTheme.colors.bgAccentRest
} else {
Color.Transparent
}
Box(
modifier = modifier
.clip(CircleShape)
.size(36.dp)
.buttonBackgroundModifier(canSendMessage)
.background(backgroundColor)
) {
Icon(
modifier = Modifier
@ -63,11 +64,7 @@ internal fun SendButtonIcon(
// Note: accessibility is managed in TextComposer.
contentDescription = null,
tint = if (canSendMessage) {
if (ElementTheme.colors.isLight) {
ElementTheme.colors.iconOnSolidPrimary
} else {
ElementTheme.colors.iconPrimary
}
} else {
ElementTheme.colors.iconQuaternary
}
@ -75,31 +72,6 @@ internal fun SendButtonIcon(
}
}
@Composable
private fun Modifier.buttonBackgroundModifier(
canSendMessage: Boolean,
) = then(
if (canSendMessage) {
val colors = gradientActionColors()
Modifier.drawWithCache {
val verticalGradientBrush = ShaderBrush(
LinearGradientShader(
from = Offset(0f, 0f),
to = Offset(0f, size.height),
colors = colors,
)
)
onDrawBehind {
drawRect(
brush = verticalGradientBrush,
)
}
}
} else {
Modifier
}
)
@PreviewsDayNight
@Composable
internal fun SendButtonIconPreview() = ElementPreview {

View file

@ -42,6 +42,7 @@ import io.element.android.libraries.textcomposer.model.SuggestionType
import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState
import io.element.android.wysiwyg.compose.RichTextEditorStyle
import io.element.android.wysiwyg.compose.internal.applyStyleInCompose
import timber.log.Timber
@Suppress("ModifierMissing")
@Composable
@ -149,8 +150,20 @@ fun MarkdownTextInput(
private fun Editable.checkSuggestionNeeded(): Suggestion? {
if (this.isEmpty()) return null
val start = Selection.getSelectionStart(this)
val end = Selection.getSelectionEnd(this)
var start = Selection.getSelectionStart(this)
var end = Selection.getSelectionEnd(this)
val range = 0..this.length
if (start !in range || end !in range) {
Timber.tag("checkSuggestionNeeded").e("Selection indices are out of bounds: start=$start, end=$end, text length=${this.length}")
return null
}
// Make sure the selection order is correct, if not swap them: sometimes we can get the end before the start
val tempEnd = end
end = maxOf(start, end)
start = minOf(start, tempEnd)
var startOfWord = start
while ((startOfWord > 0 || startOfWord == length) && !this[startOfWord - 1].isWhitespace()) {
startOfWord--
@ -161,11 +174,16 @@ private fun Editable.checkSuggestionNeeded(): Suggestion? {
// If a mention span already exists we don't need suggestions
if (getSpans<MentionSpan>(startOfWord, startOfWord + 1).isNotEmpty()) return null
return if (firstChar in listOf('@', '#', '/')) {
return if (firstChar in listOf('@', '#', '/', ':')) {
var endOfWord = end
while (endOfWord < this.length && !this[endOfWord].isWhitespace()) {
endOfWord++
}
if (startOfWord + 1 > endOfWord) {
Timber.tag("checkSuggestionNeeded").e("No need to show suggestions for an invalid range (${startOfWord + 1}..$endOfWord)")
return null
}
val text = this.subSequence(startOfWord + 1, endOfWord).toString()
val suggestionType = when (firstChar) {
'@' -> SuggestionType.Mention

View file

@ -45,7 +45,7 @@ private const val versionMonth = 3
* Release number in the month. Value must be in [0,99].
* Do not update this value. it is updated by the release script.
*/
private const val versionReleaseNumber = 2
private const val versionReleaseNumber = 3
object Versions {
/**

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a776c7cb4a3996e3fdb8366f73201ffdc693eed14bfc05482dce3e1418b25bd8
size 400144
oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a
size 399342

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6cd92a9a0de70f7f777fb72b966c72d5e2b0443b91a3da45e0b59c5061820d6
size 399861
oid sha256:02bb9e9de3b0ef480cedbed50483bdd3a899497ecc40ead72491106c6f6b6611
size 399030

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64ed3e00c2134d5f437023c48a7a1e52c7a48b954628ea26dcc3ade0a0193079
size 59670
oid sha256:6e8aabdc6d15c46ee59ba0e9d2b3b3f19500801c96501b39732d7f9f95e9130f
size 59204

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a776c7cb4a3996e3fdb8366f73201ffdc693eed14bfc05482dce3e1418b25bd8
size 400144
oid sha256:54298b08251d3bd32c451dbb2076a40f20254f78806c30c0647f1bf062f3df7a
size 399342

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c6a64aeca9d6f2b8e774d7b0582cd8d721a40de28e378553fc38606c6ef7fb6
size 59541
oid sha256:9eba0f1d35c5456b58a09ca8370c93f0d1959e8e9aa503501cc49ecfaf198522
size 59075

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:621c2bb29fa03f858a78d141a2482f701ecc265784616c52c61b7876c8ca1642
size 86580
oid sha256:e078891f5a377bf42cb787f436dcb7471de959db0c4d3afa5d7cacee20b2bf15
size 86126

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5619a51f1b95e5372d0d87cb39153455c0ebac8c2eeb679ac5cc0efb6460ec14
size 73124
oid sha256:a56951545b00dc74fd7780648bb2a508176c47df6a2ce6920f2b8a63d15f58a5
size 72675

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e815e96adf359dd888f8a71cc4727b4769948c150935c9a4af4f6158a1bf1155
size 405883
oid sha256:c0008cd2827cf805678958567bbce2ca86640a5f27e95ebf1c9cdbc0b86edfd0
size 405032

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6af0c78bd53ed3ec6a59ee7e0ae01214ae58f2d7d18ee39f134433cc628c039
size 83272
oid sha256:0b08c65638e961e2fa5f194e93c3a63ead0976b13c1b9821850c3ee865eac0a8
size 82767

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba3418ff659fcf19c24bade74c233a088e3ce71d83aea78c8aa07b06a87a69d
size 132254
oid sha256:ba76e7df81874aa6549a5f8ca7987046a4b43a63852c83fece541dc319e839d6
size 131727

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08ec28f42cc67c98eb6c3dfedc4664e42929780673f43be94d37d60f116ba84d
size 57007
oid sha256:b92c8a8283be1efed5faf6fb5f8a091f225dee38fd95e1a1b1914fa06661dc21
size 56261

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c3fa5bb746a2a005c9766b9c903d03695c5cc5124c81449519fc7a00fd55fa2
size 55831
oid sha256:c89a2eeba5fa540b6ab6516da1d8dd7810ee0754149a8a7a07cbae2182d106f5
size 55364

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72767c6e523a625e14f84c4ea299552051c09d834480c3587bfcc80acd9c59f7
size 58936
oid sha256:832b227fc82b946df042658d134f1c699c720d3e153576259fb145ec9d0c4c45
size 58441

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9e9d735218cd7cc43f104aed351dbd1b857cdbb7cf0d581316e077dc3dfb6b9
size 49248
oid sha256:b14de263a1f2356b3e84b2a633b0ed837bd160d04c1ec8eeb8c55176856e59ce
size 48755

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2f91f9b5cbad3fe3d0e87f79cef24481f758d813f90e6ef5f404860456d8e5d
size 60114
oid sha256:407301c3ad51be44fcfcd804954a672e677ea1bc59af39e4269100acc4f720d7
size 59368

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7efb7d9e793fc763e183027a01abe282f49087135430cfe7c11760b0d7cb39c
size 51622
oid sha256:473a4f7d9623a7351399d3fcf98e30adbd31feabb23efacda83fb68460b75e48
size 50912

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78ab1575bfee32b875880bc9928ec45447ff2b7ed6d6761e4d9877ca205ca47e
size 56338
oid sha256:ccf988123f7fcf7a2d6ad3914d3e1a30dcf99c49ada7fb7dbaaacb64d7f2250e
size 55562

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed4c08e8ab1fbf30b0ec52bdf3cd96a1245f55b8a055b0b938db39a550e731d6
size 50980
oid sha256:1612a4b0e0df958cf99295ab318ed8260d5c12b705f65f5db4ab2341930564b8
size 50518

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:661f615c15ce9005273638fd8d86716e186671733e1ca71b06484f87506ace86
size 53978
oid sha256:f69563c42d5315b9e4203719128340fab045ea3473c494e1a1376fe4cbb3f0d8
size 53521

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d49f25567be8b0133318295b94ea519c507d939c86c875dececf99fd37bf9257
size 44556
oid sha256:539799354ad5b383c308779b1278e879db4542d9d1c870c13a54a0ae927767fb
size 44098

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fe5d581d00cb389a432ce36b228da90fd4c120f5c362bb12ba9406fbb53a6b2a
size 59073
oid sha256:c623703f53cbf1ae42e1d05af88e3c94e5b438cb96a3dc5cd234026b29bf0e0a
size 58286

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:214e70ab7a16f9afeb9a27cf6335f238e7509eb60693b1ca2976b6095207c311
size 52506
oid sha256:ba5ae56e80ca9f5d3e5e2e0d50a2cbda7870c6286f350764dac7817d533d6c18
size 51735

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5fecd68ea15af9dd73ad56fc65b65c8b769212307467a190730648b47fe7cc55
size 8671
oid sha256:93c061fb9d41a37e4736cdbe964fe98f9814bb761e93948ec57b4ff541792f55
size 7491

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55b02c8ba432b768df833a7ba2e4745b7a48f47bb1601df3bf423e369f95bb65
size 8495
oid sha256:4bbd6e9e274341f442af1fb720e8651f8216b9b6d1bb0a2ada2477047ecfc713
size 6918

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57ee725dec5b62eeb02751da76ab86a12a3f5f6576b95d8f8f326901f9b75616
size 52182
oid sha256:35e0601856e81bf396b8457ed26ded904ec3acd005e68908c4e94a969144d0a7
size 50343

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bc4850604b7c1eb79bdb6ae143e149949a9af9c32b19aa49c56ba20987d484e
size 50396
oid sha256:ad65bc6adeac3a2679b4315db178af99aedb8e40d4952dfa7be10b87d8055dae
size 48200

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1aa0f7508a9a4e612ea3698f9343f61bd766429d73477f6607e2cddd2bc7d9cd
size 53331
oid sha256:30a1ad561aa2bc5dfa95f5494a5f016a7b4a51304765960eb71a466c1d59f3b0
size 51396

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43fe0fa0c03fb40467763adec06e21ceef05fe58a31c5098a6facda008693f87
size 51570
oid sha256:e0f904191bb608dc61c489db76b71bd66aaf9125b8138ee5696e3e2a35a3e623
size 49605

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81e733b6bc899d6bd3a6305751866277aa85b5407e930d9a649060f1a1215599
size 44786
oid sha256:008f560c95d92128a7978478563d47bbf57bc0ed1d165f69c60f3d088a8b1798
size 41741

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61c882e1d8feb6e4cad63fe10924e8876ccd0f5400f32e5b8db299cba886eac5
size 42832
oid sha256:1e8f94bfe6ac910599edd6d2a49ed0369e2304dc589cb574cbf095157e2ae507
size 39148

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9927b817b7a30b9ae2d372a666c9f0e2ea7e660738eff7fd446fa1b71b99a24b
size 52026
oid sha256:66cd907bd23752fbdea45a6505eb6548bba630088e1a6c00d8e32de3fc64ae0c
size 50427

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bde0663870910a8f5aa4d10afa501210d0dfc6b63e582432eccbc190d9ff0d2
size 50790
oid sha256:ce7294f8f9b2d847ed17970142bb54f3bf54c7365661e216d74cad1103e7739c
size 48581

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f2f40629b39b310a6e10541767b9aa6f18652ed98537558a5f7dd54ecf1e1d4
size 63109
oid sha256:f29a4e1068a8416fb78a394ed87ad195d18928a6f7c58a513f20a6b5e46372ed
size 60684

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8602db8694b0aabdfc55cef65fb530ecf807ec0c8a6436085279818cb147a937
size 60565
oid sha256:c133f56b6ba583b28684ff44e07e19ca7fc75efa6341d3e2413c2cd61a27099d
size 58169

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57ee725dec5b62eeb02751da76ab86a12a3f5f6576b95d8f8f326901f9b75616
size 52182
oid sha256:35e0601856e81bf396b8457ed26ded904ec3acd005e68908c4e94a969144d0a7
size 50343

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bc4850604b7c1eb79bdb6ae143e149949a9af9c32b19aa49c56ba20987d484e
size 50396
oid sha256:ad65bc6adeac3a2679b4315db178af99aedb8e40d4952dfa7be10b87d8055dae
size 48200

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af6ef6548666450633aac6b9449af5084065c630acd16a2d090afc89d3000d3e
size 63946
oid sha256:6dbcd0f76ceb070d1a7b3f9a22292b37c5bd9fa005915233db7bcb8f45d4ccef
size 61775

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:407c7a9cda53a42b58e11691da435ba590108b03871e8d64572950e7260b57a8
size 61044
oid sha256:335853740399624113c6e83d2cbc411691eb74759bcdcc1575b7fa3d10ad4ef6
size 58751

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c277f08b913f6b648a1df737654b341a66e937758b8d1c6d8e98b63ce62c2a9b
size 53535
oid sha256:c8cf7ee8e457e03a2abad0419fb8466f3f43b85abeabf705dd7af412dc4e4d45
size 51250

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef8e4978b968d09dfe41cedf4341e3f0fa51cce7ce5c9e1b45667a328a122920
size 50690
oid sha256:a6db26883ef045ff984e8cbf6256a01b8221825efa29b82364187c6b4fcf9d2a
size 48653

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0032f4a01bd1fab1a8db4bd3c89bd634e2cec4473844c2dd9463fdfe7faf9755
size 73375
oid sha256:acd46123208b547517a07f6e00476c40293a6c519e15acab9ef57d44658b2045
size 72217

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5695860993dcf2ad2e21262401f400d87d75d75eaa831cc8a143ce4dcfb98f1c
size 59982
oid sha256:8cfe2ba87059ddd4b442d627380813af85c55f41ee9fca2cffdf6054cee50c30
size 58821

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8de4add23d101f0ec5b04bd8d051649c4143e77d9cf41949071f9cd4299a3aa0
size 72781
oid sha256:c2fc7e9b3c12e1f0ca068a9fe5b53fc0d22926a5ec5fda56d8c5304b9a875b5f
size 71626

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1eb4fac4ebd2544d54828831809b4b116530a86316be88ad26f9ae5313dad14
size 81423
oid sha256:d45917c3e5cea88a3258044c40b553e7289a85d9b2cc8e3394ca2137081e2220
size 80237

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d7e08a93435c4380c040e63daa8e6edac47af9946d3075b80abd7f71e53d5ac
size 62489
oid sha256:5d50faa01b0567152ca88b187d602f48c260048484ed9576d685d0a635d47c68
size 61337

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b3e1905c73b7561c8400c5426bd7c7d95897d9ced5de3212d1f56d806053e1b
size 61416
oid sha256:112c9de20f972933b15936fb1f1e4d56457df66d72075845c56f88cf261dba76
size 60253

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91e00c997726832f255ce5b376213e1160279f172b60676d853c981cc4b7afb8
size 68346
oid sha256:cf49f2a364dc2cc054dbd81a67c9ddafb2cb8711c9300f0ac8f1f9c4e3f77ab4
size 67206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55734a9a9725123cbf1d0ebef46579d3a3939b2898f7144e529b71d447456577
size 90320
oid sha256:b53e8135e3b07a6a4a89def1735146e860c2d52e35e8a4af694b8e872b38e13a
size 89219

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8f73bcb2919e607d76dbc85ce82d8c8bf9029708bc2b3d21f212a5b8f8c187f
size 60826
oid sha256:21899812bbb9d0cf288911fe681626fcc978a6310527e242610d3392d4a4ef66
size 59667

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f36ea905ab1718fbe97a89171c21605cab4396dec6a83a0465b945fa2cf2fa79
size 62046
oid sha256:1cb7578c689fd3cd81236d25d8853f6203a91d8879600e72f23e80c86d8b68d9
size 60860

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6adcff4197af21036e0f3ea834739ce50e700484ba60ae343905df5926b5f0d
size 68136
oid sha256:d23fdafbd834da43a6fe57d5666fc6a3108359838053961a419a9b90a1f7aa53
size 67001

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:212b7c60ce30503bac88dbb14dc2a7343f728039286e4c54e7bc5b82a06dce6b
size 60393
oid sha256:7f70b173214fb5971f3db853c85217f7600e9333a6bd411c11e3f1aee4c4e932
size 59220

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f27bcfe743451f511f73aa5c096db8a424ce8ecbe199d8cb920010a07037a18
size 71114
oid sha256:fea3bacf1a1927a05b2b515fde06d176e102485474cbed004a781e8d2d7385eb
size 69296

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3314f19a32c7a8f3ca0df6262271a60760f7cecde60879bf54b26e398197801e
size 58113
oid sha256:7661c233ef056af6e014a117530d55e336935ad071467a75f2d165657f33aa7c
size 56339

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03cf43fba473e09ce73354c3ee5e9fa7d051243610eedcdd576a1fcf8c21d4b0
size 70560
oid sha256:b8b91059084d9cd410414fcea6dc6b3489969f062184834ad89ac9ad5b9c2ee9
size 68744

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:85f12e1a86dd50f69766205b627e7db7c5a6b0f37c0c921c148783a1ebfa9bbf
size 78970
oid sha256:2367a9ca1ad216529eb5ca70406fa28788cd9ef2c3fed9dd66e9a5e4acf3080c
size 77172

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d0d91f146f43abfd271025b586d87e0153aac00803bed1682fb381bc2750d8d
size 60652
oid sha256:efb5dcb70030f73913dc05ffdb70f757f71b615372e02d9e21bbaa9eff91befd
size 58801

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70a9665d9aa337efdd4ac4beedab6dbaddc1e74b41e43499da8351fb59fdfa1f
size 59573
oid sha256:f92132cbeeb5543e8c4f1e1221b07da57dd6f3abc119c46ec291f240f1e5a522
size 57759

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd5b3afa248eb6486c3be193a48218ace0c3b62c32961ccc4e769cdd822de612
size 66348
oid sha256:895ba3794457f13dcb3e59499924be167f8408ce6e3782a7b26b452226bbca6a
size 64540

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88b66579a22b2097bad6fb912e646d6e08905fdc187629a21ba6e469faf63883
size 87210
oid sha256:c1df5a7f329260f6c019207ff66b5df2aab8f9d3fff026ea695111361b46c202
size 85539

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f22d7e6659dbe26557cc766d156461ccb93d46c725c7c8acd7f0a268103dcc8
size 59008
oid sha256:b3f7417d9135f5b502429a7030a8531a60f45044a5518806286d1f28487c3a21
size 57198

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f86b4b4191628ed78d7e5d281eb755e36f59f02160f850cedeafa6d03cd2a2c
size 60115
oid sha256:316ac7e8b79f52192a8c699e903525be92b9d51c86242157b724a59b39c4dcc6
size 58254

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d55e7cacfb2ca44438c9d8e145c35f52966ef9281da0745d5d8806d698b6e9dc
size 65958
oid sha256:df811a1843d3d88ac021d8df892c2b74f61a679317ed40234438459833b90fa9
size 64134

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5351b897a218d2d649c7e6e34e1f187421e786754d69bc007509b77f931b2248
size 58580
oid sha256:3dddaa9eba56b7c02c7e42add553fcf24b3ded18488bfe75e0decc7270386b29
size 56795

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:591e982b4aa78f9302b6e2776a5d8a8f80541397fbf32e4b9bafce44b6b10bb8
size 75446
oid sha256:cf7b37ce6ac1d6430de6d4ffefc2864f0adc502a395a204b3c516dbd901f9710
size 73331

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c75ba9c2d470fb6332b140b798be06364b8c1982f997fc3e8ab5048c5a38f3df
size 59060
oid sha256:c18604b8f5aaed6e4093d3f792ad112668402541ac09547bdebd6a5adbfca35b
size 56876

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