Merge branch 'release/0.3.2'
This commit is contained in:
commit
d13d4f4aa7
688 changed files with 6169 additions and 2493 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -55,7 +55,7 @@ jobs:
|
||||||
name: elementx-debug
|
name: elementx-debug
|
||||||
path: |
|
path: |
|
||||||
app/build/outputs/apk/debug/*.apk
|
app/build/outputs/apk/debug/*.apk
|
||||||
- uses: rnkdsh/action-upload-diawi@v1.5.3
|
- uses: rnkdsh/action-upload-diawi@v1.5.4
|
||||||
id: diawi
|
id: diawi
|
||||||
# Do not fail the whole build if Diawi upload fails
|
# Do not fail the whole build if Diawi upload fails
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
|
||||||
2
.github/workflows/maestro.yml
vendored
2
.github/workflows/maestro.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
- uses: mobile-dev-inc/action-maestro-cloud@v1.6.0
|
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.0
|
||||||
with:
|
with:
|
||||||
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
|
||||||
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ appId: ${APP_ID}
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
text: "Report a problem"
|
text: "Report a problem"
|
||||||
- assertVisible: "Report a bug"
|
- assertVisible: "Report a problem"
|
||||||
- back
|
- back
|
||||||
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
|
|
|
||||||
25
CHANGES.md
25
CHANGES.md
|
|
@ -1,3 +1,28 @@
|
||||||
|
Changes in Element X v0.3.2 (2023-11-22)
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Features ✨
|
||||||
|
----------
|
||||||
|
- Add ongoing call indicator to rooms lists items. ([#1158](https://github.com/vector-im/element-x-android/issues/1158))
|
||||||
|
- Add support for typing mentions in the message composer. ([#1453](https://github.com/vector-im/element-x-android/issues/1453))
|
||||||
|
- Add intentional mentions to messages. This needs to be enabled in developer options since it's disabled by default. ([#1591](https://github.com/vector-im/element-x-android/issues/1591))
|
||||||
|
- Update voice message recording behaviour. Instead of holding the record button, users can now tap the record button to start recording and tap again to stop recording. ([#1784](https://github.com/vector-im/element-x-android/issues/1784))
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Always ensure media temp dir exists ([#1790](https://github.com/vector-im/element-x-android/issues/1790))
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Update icons and move away from `PreferenceText` components. ([#1718](https://github.com/vector-im/element-x-android/issues/1718))
|
||||||
|
- Add item "This is the beginning of..." at the beginning of the timeline. ([#1801](https://github.com/vector-im/element-x-android/issues/1801))
|
||||||
|
- LockScreen : rework LoggedInFlowNode and back management when locked. ([#1806](https://github.com/vector-im/element-x-android/issues/1806))
|
||||||
|
- Suppress usage of removeTimeline method. ([#1824](https://github.com/vector-im/element-x-android/issues/1824))
|
||||||
|
- Remove Element Call feature flag, it's now always enabled.
|
||||||
|
- Reverted the EC base URL to `https://call.element.io`.
|
||||||
|
- Moved the option to override this URL to developer settings from advanced settings.
|
||||||
|
|
||||||
|
|
||||||
Changes in Element X v0.3.1 (2023-11-09)
|
Changes in Element X v0.3.1 (2023-11-09)
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
api(libs.anvil.compiler.api)
|
api(libs.anvil.compiler.api)
|
||||||
implementation(libs.anvil.compiler.utils)
|
implementation(libs.anvil.compiler.utils)
|
||||||
implementation("com.squareup:kotlinpoet:1.14.2")
|
implementation(libs.kotlinpoet)
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
compileOnly(libs.google.autoservice.annotations)
|
compileOnly(libs.google.autoservice.annotations)
|
||||||
kapt(libs.google.autoservice)
|
kapt(libs.google.autoservice)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ plugins {
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.kapt)
|
alias(libs.plugins.kapt)
|
||||||
id("com.google.firebase.appdistribution") version "4.0.1"
|
alias(libs.plugins.firebaseAppDistribution)
|
||||||
id("org.jetbrains.kotlinx.knit") version "0.4.0"
|
alias(libs.plugins.knit)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
// To be able to update the firebase.xml files, uncomment and build the project
|
// To be able to update the firebase.xml files, uncomment and build the project
|
||||||
// id("com.google.gms.google-services")
|
// id("com.google.gms.google-services")
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.x
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.startup.AppInitializer
|
import androidx.startup.AppInitializer
|
||||||
|
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
import io.element.android.x.di.AppComponent
|
import io.element.android.x.di.AppComponent
|
||||||
import io.element.android.x.di.DaggerAppComponent
|
import io.element.android.x.di.DaggerAppComponent
|
||||||
|
|
@ -27,17 +28,14 @@ import io.element.android.x.initializer.TracingInitializer
|
||||||
|
|
||||||
class ElementXApplication : Application(), DaggerComponentOwner {
|
class ElementXApplication : Application(), DaggerComponentOwner {
|
||||||
|
|
||||||
private lateinit var appComponent: AppComponent
|
override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this)
|
||||||
|
|
||||||
override val daggerComponent: Any
|
|
||||||
get() = appComponent
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
appComponent = DaggerAppComponent.factory().create(applicationContext)
|
|
||||||
AppInitializer.getInstance(this).apply {
|
AppInitializer.getInstance(this).apply {
|
||||||
initializeComponent(CrashInitializer::class.java)
|
initializeComponent(CrashInitializer::class.java)
|
||||||
initializeComponent(TracingInitializer::class.java)
|
initializeComponent(TracingInitializer::class.java)
|
||||||
|
initializeComponent(CacheCleanerInitializer::class.java)
|
||||||
}
|
}
|
||||||
logApplicationInfo()
|
logApplicationInfo()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
|
@ -33,10 +36,14 @@ import com.bumble.appyx.core.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||||
import io.element.android.features.lockscreen.api.handleSecureFlag
|
import io.element.android.features.lockscreen.api.handleSecureFlag
|
||||||
|
import io.element.android.features.lockscreen.api.isLocked
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.theme.theme.Theme
|
||||||
|
import io.element.android.libraries.theme.theme.isDark
|
||||||
|
import io.element.android.libraries.theme.theme.mapToTheme
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
import io.element.android.x.intent.SafeUriHandler
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -46,7 +53,6 @@ private val loggerTag = LoggerTag("MainActivity")
|
||||||
class MainActivity : NodeActivity() {
|
class MainActivity : NodeActivity() {
|
||||||
|
|
||||||
private lateinit var mainNode: MainNode
|
private lateinit var mainNode: MainNode
|
||||||
|
|
||||||
private lateinit var appBindings: AppBindings
|
private lateinit var appBindings: AppBindings
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -61,9 +67,29 @@ class MainActivity : NodeActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
override fun onBackPressed() {
|
||||||
|
// If the app is locked, we need to intercept onBackPressed before it goes to OnBackPressedDispatcher.
|
||||||
|
// Indeed, otherwise we would need to trick Appyx backstack management everywhere.
|
||||||
|
// Without this trick, we would get pop operations on the hidden backstack.
|
||||||
|
if (appBindings.lockScreenService().isLocked) {
|
||||||
|
// Do not kill the app in this case, just go to background.
|
||||||
|
moveTaskToBack(false)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainContent(appBindings: AppBindings) {
|
private fun MainContent(appBindings: AppBindings) {
|
||||||
ElementTheme {
|
val theme by remember {
|
||||||
|
appBindings.preferencesStore().getThemeFlow().mapToTheme()
|
||||||
|
}
|
||||||
|
.collectAsState(initial = Theme.System)
|
||||||
|
ElementTheme(
|
||||||
|
darkTheme = theme.isDark()
|
||||||
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||||
LocalUriHandler provides SafeUriHandler(this),
|
LocalUriHandler provides SafeUriHandler(this),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -29,4 +30,5 @@ interface AppBindings {
|
||||||
fun tracingService(): TracingService
|
fun tracingService(): TracingService
|
||||||
fun bugReporter(): BugReporter
|
fun bugReporter(): BugReporter
|
||||||
fun lockScreenService(): LockScreenService
|
fun lockScreenService(): LockScreenService
|
||||||
|
fun preferencesStore(): PreferencesStore
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@
|
||||||
package io.element.android.appconfig
|
package io.element.android.appconfig
|
||||||
|
|
||||||
object ElementCallConfig {
|
object ElementCallConfig {
|
||||||
const val DEFAULT_BASE_URL = "https://call.element.dev"
|
const val DEFAULT_BASE_URL = "https://call.element.io"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.config
|
package io.element.android.appconfig
|
||||||
|
|
||||||
object PushConfig {
|
object PushConfig {
|
||||||
/**
|
/**
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appconfig
|
||||||
|
|
||||||
|
object TimelineConfig {
|
||||||
|
const val maxReadReceiptToDisplay = 3
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.appconfig
|
||||||
|
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
object VoiceMessageConfig {
|
||||||
|
val maxVoiceMessageDuration = 30.minutes
|
||||||
|
}
|
||||||
|
|
@ -362,23 +362,18 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
||||||
when (lockScreenState) {
|
Children(
|
||||||
LockScreenLockState.Unlocked -> {
|
navModel = backstack,
|
||||||
Children(
|
modifier = Modifier,
|
||||||
navModel = backstack,
|
// Animate navigation to settings and to a room
|
||||||
modifier = Modifier,
|
transitionHandler = rememberDefaultTransitionHandler(),
|
||||||
// Animate navigation to settings and to a room
|
)
|
||||||
transitionHandler = rememberDefaultTransitionHandler(),
|
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
||||||
)
|
if (!isFtueDisplayed) {
|
||||||
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
if (!isFtueDisplayed) {
|
}
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
if (lockScreenState == LockScreenLockState.Locked) {
|
||||||
}
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
||||||
}
|
|
||||||
LockScreenLockState.Locked -> {
|
|
||||||
MoveActivityToBackgroundBackHandler()
|
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
|
classpath(libs.kotlin.gradle.plugin)
|
||||||
classpath("com.google.gms:google-services:4.4.0")
|
classpath(libs.gms.google.services)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
fastlane/metadata/android/en-US/changelogs/40003020.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/40003020.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Main changes in this version:
|
||||||
|
- Element Call is now enabled by default.
|
||||||
|
- There is an 'ongoing call' indicator in the room list.
|
||||||
|
- Adding mentions to a message is now possible, but it's disabled by default as it's a work in progress. They can be enabled in the developer options.
|
||||||
|
- Voice messages behavior changed: there is no need to keep pressing to record, to start recording a message just tap on the mic button, then tap again to stop recording.
|
||||||
|
- Added a marker in the timeline to indicate the starting point of the room messages.
|
||||||
|
Full changelog: https://github.com/vector-im/element-x-android/releases
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||||
|
import io.element.android.features.analytics.api.R
|
||||||
import io.element.android.libraries.designsystem.components.LINK_TAG
|
import io.element.android.libraries.designsystem.components.LINK_TAG
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -30,7 +31,6 @@ import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithSt
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
import io.element.android.libraries.designsystem.theme.components.ListSupportingText
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalyticsPreferencesView(
|
fun AnalyticsPreferencesView(
|
||||||
|
|
@ -42,18 +42,18 @@ fun AnalyticsPreferencesView(
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportingText = stringResource(
|
val supportingText = stringResource(
|
||||||
id = CommonStrings.screen_analytics_settings_help_us_improve,
|
id = R.string.screen_analytics_settings_help_us_improve,
|
||||||
state.applicationName
|
state.applicationName
|
||||||
)
|
)
|
||||||
val linkText = buildAnnotatedStringWithStyledPart(
|
val linkText = buildAnnotatedStringWithStyledPart(
|
||||||
CommonStrings.screen_analytics_settings_read_terms,
|
R.string.screen_analytics_settings_read_terms,
|
||||||
CommonStrings.screen_analytics_settings_read_terms_content_link,
|
R.string.screen_analytics_settings_read_terms_content_link,
|
||||||
tagAndLink = LINK_TAG to state.policyUrl,
|
tagAndLink = LINK_TAG to state.policyUrl,
|
||||||
)
|
)
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data))
|
Text(stringResource(id = R.string.screen_analytics_settings_share_data))
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(supportingText)
|
Text(supportingText)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Sdílet analytická data"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Můžete si přečíst všechny naše podmínky %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"zde"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Analysedaten teilen"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Teile anonyme Nutzungsdaten, um uns bei der Identifizierung von Problemen zu helfen."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Du kannst alle unsere Bedingungen lesen %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"hier"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Partagez des données de statistiques d’utilisation"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Vous pouvez lire toutes nos conditions %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"ici"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Partajați datele analitice"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Distribuiți date anonime de utilizare pentru a ne ajuta să identificăm probleme."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Puteți citi toate condițiile noastre %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"aici"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Делитесь данными аналитики"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Вы можете ознакомиться со всеми нашими условиями %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"здесь"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Zdieľať analytické údaje"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Zdieľajte anonymné údaje o používaní, aby sme mohli identifikovať problémy."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"Môžete si prečítať všetky naše podmienky %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"tu"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"分享分析數據"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"分享匿名的使用數據以協助我們釐清問題。"</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"這裡"</string>
|
||||||
|
</resources>
|
||||||
7
features/analytics/api/src/main/res/values/localazy.xml
Normal file
7
features/analytics/api/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_analytics_settings_share_data">"Share analytics data"</string>
|
||||||
|
<string name="screen_analytics_settings_help_us_improve">"Share anonymous usage data to help us identify issues."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms">"You can read all our terms %1$s."</string>
|
||||||
|
<string name="screen_analytics_settings_read_terms_content_link">"here"</string>
|
||||||
|
</resources>
|
||||||
29
features/cachecleaner/api/build.gradle.kts
Normal file
29
features/cachecleaner/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.cachecleaner.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(libs.androidx.startup)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.cachecleaner.api
|
||||||
|
|
||||||
|
interface CacheCleaner {
|
||||||
|
/**
|
||||||
|
* Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages).
|
||||||
|
*
|
||||||
|
* Will fail silently in case of errors while deleting the files.
|
||||||
|
*/
|
||||||
|
fun clearCache()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.cachecleaner.api
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
|
@ContributesTo(AppScope::class)
|
||||||
|
interface CacheCleanerBindings {
|
||||||
|
fun cacheCleaner(): CacheCleaner
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.cachecleaner.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.startup.Initializer
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
|
||||||
|
class CacheCleanerInitializer : Initializer<Unit> {
|
||||||
|
override fun create(context: Context) {
|
||||||
|
context.bindings<CacheCleanerBindings>().cacheCleaner().clearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
||||||
|
}
|
||||||
41
features/cachecleaner/impl/build.gradle.kts
Normal file
41
features/cachecleaner/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.cachecleaner.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.anvilannotations)
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
|
api(projects.features.cachecleaner.api)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.cachecleaner.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.cachecleaner.api.CacheCleaner
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.CacheDirectory
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [CacheCleaner].
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultCacheCleaner @Inject constructor(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
@CacheDirectory private val cacheDir: File,
|
||||||
|
) : CacheCleaner {
|
||||||
|
companion object {
|
||||||
|
val SUBDIRS_TO_CLEANUP = listOf("temp/media", "temp/voice")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearCache() {
|
||||||
|
scope.launch(dispatchers.io) {
|
||||||
|
runCatching {
|
||||||
|
SUBDIRS_TO_CLEANUP.forEach {
|
||||||
|
File(cacheDir.path, it).apply {
|
||||||
|
if (exists()) {
|
||||||
|
if (!deleteRecursively()) error("Failed to delete recursively cache directory $this")
|
||||||
|
}
|
||||||
|
if (!mkdirs()) error("Failed to create cache directory $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it, "Failed to clear cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.cachecleaner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class DefaultCacheCleanerTest {
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calling clearCache actually removes file in the SUBDIRS_TO_CLEANUP list`() = runTest {
|
||||||
|
// Create temp subdirs and fill with 2 files each
|
||||||
|
DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach {
|
||||||
|
File(temporaryFolder.root, it).apply {
|
||||||
|
mkdirs()
|
||||||
|
File(this, "temp1").createNewFile()
|
||||||
|
File(this, "temp2").createNewFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
aCacheCleaner().clearCache()
|
||||||
|
|
||||||
|
// Check the files are gone but the sub dirs are not.
|
||||||
|
DefaultCacheCleaner.SUBDIRS_TO_CLEANUP.forEach {
|
||||||
|
File(temporaryFolder.root, it).apply {
|
||||||
|
Truth.assertThat(exists()).isTrue()
|
||||||
|
Truth.assertThat(isDirectory).isTrue()
|
||||||
|
Truth.assertThat(listFiles()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clear cache fails silently`() = runTest {
|
||||||
|
// Set cache dir as unreadable, unwritable and unexecutable so that the deletion fails.
|
||||||
|
check(temporaryFolder.root.setReadable(false))
|
||||||
|
check(temporaryFolder.root.setWritable(false))
|
||||||
|
check(temporaryFolder.root.setExecutable(false))
|
||||||
|
|
||||||
|
aCacheCleaner().clearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.aCacheCleaner() = DefaultCacheCleaner(
|
||||||
|
scope = this,
|
||||||
|
dispatchers = this.testCoroutineDispatchers(true),
|
||||||
|
cacheDir = temporaryFolder.root,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -31,15 +31,22 @@ import android.webkit.PermissionRequest
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
|
||||||
import io.element.android.features.call.CallForegroundService
|
import io.element.android.features.call.CallForegroundService
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.CallType
|
||||||
import io.element.android.features.call.di.CallBindings
|
import io.element.android.features.call.di.CallBindings
|
||||||
import io.element.android.features.call.utils.CallIntentDataParser
|
import io.element.android.features.call.utils.CallIntentDataParser
|
||||||
|
import io.element.android.features.preferences.api.store.PreferencesStore
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.theme.theme.Theme
|
||||||
|
import io.element.android.libraries.theme.theme.isDark
|
||||||
|
import io.element.android.libraries.theme.theme.mapToTheme
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
@ -60,6 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
|
@Inject lateinit var preferencesStore: PreferencesStore
|
||||||
|
|
||||||
private lateinit var presenter: CallScreenPresenter
|
private lateinit var presenter: CallScreenPresenter
|
||||||
|
|
||||||
|
|
@ -92,8 +100,14 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
requestAudioFocus()
|
requestAudioFocus()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
val theme by remember {
|
||||||
|
preferencesStore.getThemeFlow().mapToTheme()
|
||||||
|
}
|
||||||
|
.collectAsState(initial = Theme.System)
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
ElementTheme {
|
ElementTheme(
|
||||||
|
darkTheme = theme.isDark()
|
||||||
|
) {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
requestPermissions = { permissions, callback ->
|
requestPermissions = { permissions, callback ->
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ private fun CreateRoomActionButtonsList(
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
CreateRoomActionButton(
|
CreateRoomActionButton(
|
||||||
iconRes = CommonDrawables.ic_groups,
|
iconRes = CommonDrawables.ic_compound_plus,
|
||||||
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
text = stringResource(id = R.string.screen_create_room_action_create_room),
|
||||||
onClick = onNewRoomClicked,
|
onClick = onNewRoomClicked,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,19 +48,19 @@ private fun LeaveRoomConfirmationDialog(
|
||||||
when (state.confirmation) {
|
when (state.confirmation) {
|
||||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||||
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.PrivateRoom -> LeaveRoomConfirmationDialog(
|
||||||
text = CommonStrings.leave_room_alert_private_subtitle,
|
text = R.string.leave_room_alert_private_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.LastUserInRoom -> LeaveRoomConfirmationDialog(
|
||||||
text = CommonStrings.leave_room_alert_empty_subtitle,
|
text = R.string.leave_room_alert_empty_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.Generic -> LeaveRoomConfirmationDialog(
|
||||||
text = CommonStrings.leave_room_alert_subtitle,
|
text = R.string.leave_room_alert_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_room_alert_empty_subtitle">"Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Opravdu chcete opustit místnost?"</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="leave_room_alert_empty_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Du bist die einzige Person hier. Wenn du austritst, kann in Zukunft niemand mehr eintreten, auch du nicht."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Bist du sicher, dass du diesen Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne Einladung nicht erneut beitreten."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Bist du sicher, dass du den Raum verlassen willst?"</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="leave_room_alert_empty_subtitle">"¿Estás seguro de que quieres salir de esta sala? Eres la única persona aquí. Si te vas, nadie podrá unirse en el futuro, ni siquiera tú."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"¿Estás seguro de que quieres abandonar esta sala? Esta sala no es pública y no podrás volver a entrar sin una invitación."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"¿Seguro que quieres salir de la habitación?"</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="leave_room_alert_empty_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Êtes-vous sûr de vouloir quitter le salon ?"</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="leave_room_alert_empty_subtitle">"Sei sicuro di voler lasciare questa stanza? Sei l\'unica persona presente. Se esci, nessuno potrà unirsi in futuro, te compreso."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Sei sicuro di voler lasciare questa stanza? Questa stanza non è pubblica e non potrai rientrare senza un invito."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Sei sicuro di voler lasciare la stanza?"</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="leave_room_alert_empty_subtitle">"Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Sunteți sigur că vreți să părăsiți camera?"</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="leave_room_alert_empty_subtitle">"Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Вы уверены, что хотите покинуть комнату?"</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="leave_room_alert_empty_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Ste tu jediná osoba. Ak odídete, nikto sa do nej nebude môcť v budúcnosti pripojiť, vrátane vás."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Ste si istí, že chcete opustiť miestnosť?"</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="leave_room_alert_empty_subtitle">"您確定要離開聊天室嗎?這裡只有您一個人。如果您離開了,包含您在內的所有人都無法再進入此聊天室。"</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"您確定要離開聊天室嗎?此聊天室不是公開的,如果沒有收到邀請,您無法重新加入。"</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"您確定要離開聊天室嗎?"</string>
|
||||||
|
</resources>
|
||||||
6
features/leaveroom/api/src/main/res/values/localazy.xml
Normal file
6
features/leaveroom/api/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="leave_room_alert_empty_subtitle">"Are you sure that you want to leave this room? You\'re the only person here. If you leave, no one will be able to join in the future, including you."</string>
|
||||||
|
<string name="leave_room_alert_private_subtitle">"Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite."</string>
|
||||||
|
<string name="leave_room_alert_subtitle">"Are you sure that you want to leave the room?"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -18,7 +18,6 @@ package io.element.android.features.location.impl.send
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
|
@ -27,9 +26,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.LocationSearching
|
|
||||||
import androidx.compose.material.icons.filled.MyLocation
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.SheetValue
|
import androidx.compose.material3.SheetValue
|
||||||
|
|
@ -68,7 +64,7 @@ import io.element.android.libraries.maplibre.compose.rememberCameraPositionState
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SendLocationView(
|
fun SendLocationView(
|
||||||
state: SendLocationState,
|
state: SendLocationState,
|
||||||
|
|
@ -215,8 +211,8 @@ fun SendLocationView(
|
||||||
.padding(end = 16.dp, bottom = 72.dp + navBarPadding),
|
.padding(end = 16.dp, bottom = 72.dp + navBarPadding),
|
||||||
) {
|
) {
|
||||||
when (state.mode) {
|
when (state.mode) {
|
||||||
SendLocationState.Mode.PinLocation -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
|
SendLocationState.Mode.PinLocation -> Icon(resourceId = CommonDrawables.ic_location_navigator, contentDescription = null)
|
||||||
SendLocationState.Mode.SenderLocation -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
|
SendLocationState.Mode.SenderLocation -> Icon(resourceId = CommonDrawables.ic_location_navigator_centered, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ interface LockScreenService {
|
||||||
fun isPinSetup(): Flow<Boolean>
|
fun isPinSetup(): Flow<Boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the app is currently locked.
|
||||||
|
*/
|
||||||
|
val LockScreenService.isLocked: Boolean
|
||||||
|
get() = lockState.value == LockScreenLockState.Locked
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sure the secure flag is set on the activity if the pin is setup.
|
* Makes sure the secure flag is set on the activity if the pin is setup.
|
||||||
* @param activity the activity to set the flag on.
|
* @param activity the activity to set the flag on.
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@
|
||||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Сэкономьте время и используйте %1$s для разблокировки приложения"</string>
|
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Сэкономьте время и используйте %1$s для разблокировки приложения"</string>
|
||||||
<string name="screen_app_lock_setup_choose_pin">"Выберите PIN-код"</string>
|
<string name="screen_app_lock_setup_choose_pin">"Выберите PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_confirm_pin">"Подтвердите PIN-код"</string>
|
<string name="screen_app_lock_setup_confirm_pin">"Подтвердите PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Из соображений безопасности вы не можешь выбрать это в качестве PIN-кода."</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Из соображений безопасности вы не можешь выбрать это в качестве PIN-кода"</string>
|
||||||
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Выберите другой PIN-код"</string>
|
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Выберите другой PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_context">"Заблокируйте %1$s, чтобы повысить безопасность ваших чатов.
|
<string name="screen_app_lock_setup_pin_context">"Заблокируйте %1$s, чтобы повысить безопасность ваших чатов.
|
||||||
|
|
||||||
Выберите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."</string>
|
Выберите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Повторите PIN-код"</string>
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PIN-коды не совпадают"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код."</string>
|
<string name="screen_app_lock_signout_alert_message">"Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"Вы выходите из системы"</string>
|
<string name="screen_app_lock_signout_alert_title">"Вы выходите из системы"</string>
|
||||||
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
|
<string name="screen_app_lock_use_biometric_android">"Использовать биометрию"</string>
|
||||||
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
|
<string name="screen_app_lock_use_pin_android">"Использовать PIN-код"</string>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ plugins {
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.9.20"
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
<string name="screen_waitlist_message">"В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова.
|
<string name="screen_waitlist_message">"В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова.
|
||||||
|
|
||||||
Спасибо за терпение!"</string>
|
Спасибо за терпение!"</string>
|
||||||
<string name="screen_waitlist_title">"Почти готово!"</string>
|
<string name="screen_waitlist_title">"Почти готово."</string>
|
||||||
<string name="screen_waitlist_title_success">"Вы зарегистрированы!"</string>
|
<string name="screen_waitlist_title_success">"Вы зарегистрированы."</string>
|
||||||
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
<string name="screen_login_subtitle">"Matrix — это открытая сеть для безопасной децентрализованной связи."</string>
|
||||||
<string name="screen_waitlist_message_success">"Добро пожаловать в %1$s!"</string>
|
<string name="screen_waitlist_message_success">"Добро пожаловать в %1$s!"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Opravdu se chcete odhlásit?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Opravdu se chcete odhlásit?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Odhlásit se"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Odhlašování…"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"Vypnuli jste zálohování"</string>
|
<string name="screen_signout_key_backup_disabled_title">"Vypnuli jste zálohování"</string>
|
||||||
|
|
@ -14,5 +13,6 @@
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Uložili jste si klíč pro obnovení?"</string>
|
<string name="screen_signout_save_recovery_key_title">"Uložili jste si klíč pro obnovení?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Odhlásit se"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Odhlásit se"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Odhlásit se"</string>
|
||||||
<string name="screen_signout_preference_item">"Odhlásit se"</string>
|
<string name="screen_signout_preference_item">"Odhlásit se"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Bist du sicher, dass du dich abmelden willst?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Bist du sicher, dass du dich abmelden willst?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
|
|
||||||
<string name="screen_signout_preference_item">"Abmelden"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
|
|
||||||
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Êtes-vous sûr de vouloir vous déconnecter ?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Êtes-vous sûr de vouloir vous déconnecter ?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Se déconnecter"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Déconnexion…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Déconnexion…"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"Vous avez désactivé la sauvegarde"</string>
|
<string name="screen_signout_key_backup_disabled_title">"Vous avez désactivé la sauvegarde"</string>
|
||||||
|
|
@ -14,5 +13,6 @@
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Avez-vous sauvegardé votre clé de récupération?"</string>
|
<string name="screen_signout_save_recovery_key_title">"Avez-vous sauvegardé votre clé de récupération?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Se déconnecter"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Se déconnecter"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Se déconnecter"</string>
|
||||||
<string name="screen_signout_preference_item">"Se déconnecter"</string>
|
<string name="screen_signout_preference_item">"Se déconnecter"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Esci"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Esci"</string>
|
|
||||||
<string name="screen_signout_preference_item">"Esci"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Deconectați-vă"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Deconectați-vă"</string>
|
|
||||||
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Вы уверены, что вы хотите выйти?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Вы уверены, что вы хотите выйти?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Выйти"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Выполняется выход…"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"Вы отключили резервное копирование"</string>
|
<string name="screen_signout_key_backup_disabled_title">"Вы отключили резервное копирование"</string>
|
||||||
|
|
@ -14,5 +13,6 @@
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Вы сохранили свой ключ восстановления?"</string>
|
<string name="screen_signout_save_recovery_key_title">"Вы сохранили свой ключ восстановления?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Выйти"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Выйти"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Выйти"</string>
|
||||||
<string name="screen_signout_preference_item">"Выйти"</string>
|
<string name="screen_signout_preference_item">"Выйти"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Ste si istí, že sa chcete odhlásiť?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Ste si istí, že sa chcete odhlásiť?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Odhlásiť sa"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Prebieha odhlasovanie…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Prebieha odhlasovanie…"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"Vypli ste zálohovanie"</string>
|
<string name="screen_signout_key_backup_disabled_title">"Vypli ste zálohovanie"</string>
|
||||||
|
|
@ -14,5 +13,6 @@
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Uložili ste si kľúč na obnovenie?"</string>
|
<string name="screen_signout_save_recovery_key_title">"Uložili ste si kľúč na obnovenie?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Odhlásiť sa"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Odhlásiť sa"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Odhlásiť sa"</string>
|
||||||
<string name="screen_signout_preference_item">"Odhlásiť sa"</string>
|
<string name="screen_signout_preference_item">"Odhlásiť sa"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"您確定要登出嗎?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"您確定要登出嗎?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"登出"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"正在登出…"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"登出"</string>
|
|
||||||
<string name="screen_signout_preference_item">"登出"</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_signout_confirmation_dialog_content">"Are you sure you want to sign out?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Are you sure you want to sign out?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Sign out"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||||
<string name="screen_signout_key_backup_disabled_subtitle">"You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."</string>
|
<string name="screen_signout_key_backup_disabled_subtitle">"You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."</string>
|
||||||
<string name="screen_signout_key_backup_disabled_title">"You have turned off backup"</string>
|
<string name="screen_signout_key_backup_disabled_title">"You have turned off backup"</string>
|
||||||
|
|
@ -14,5 +13,6 @@
|
||||||
<string name="screen_signout_save_recovery_key_subtitle">"You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."</string>
|
<string name="screen_signout_save_recovery_key_subtitle">"You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."</string>
|
||||||
<string name="screen_signout_save_recovery_key_title">"Have you saved your recovery key?"</string>
|
<string name="screen_signout_save_recovery_key_title">"Have you saved your recovery key?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Sign out"</string>
|
<string name="screen_signout_confirmation_dialog_submit">"Sign out"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_title">"Sign out"</string>
|
||||||
<string name="screen_signout_preference_item">"Sign out"</string>
|
<string name="screen_signout_preference_item">"Sign out"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
anvil(projects.anvilcodegen)
|
anvil(projects.anvilcodegen)
|
||||||
api(projects.features.messages.api)
|
api(projects.features.messages.api)
|
||||||
|
implementation(projects.appconfig)
|
||||||
implementation(projects.features.call)
|
implementation(projects.features.call)
|
||||||
implementation(projects.features.location.api)
|
implementation(projects.features.location.api)
|
||||||
implementation(projects.features.poll.api)
|
implementation(projects.features.poll.api)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
|
|
@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val customReactionPresenter: CustomReactionPresenter,
|
private val customReactionPresenter: CustomReactionPresenter,
|
||||||
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
private val reactionSummaryPresenter: ReactionSummaryPresenter,
|
||||||
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
private val retrySendMenuPresenter: RetrySendMenuPresenter,
|
||||||
|
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
|
|
@ -124,6 +126,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val customReactionState = customReactionPresenter.present()
|
val customReactionState = customReactionPresenter.present()
|
||||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||||
val retryState = retrySendMenuPresenter.present()
|
val retryState = retrySendMenuPresenter.present()
|
||||||
|
val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present()
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||||
|
|
@ -157,10 +160,10 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
|
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
|
||||||
|
|
||||||
var enableVoiceMessages by remember { mutableStateOf(false) }
|
var enableVoiceMessages by remember { mutableStateOf(false) }
|
||||||
var enableInRoomCalls by remember { mutableStateOf(false) }
|
// TODO add min power level to use this feature in the future?
|
||||||
|
val enableInRoomCalls = true
|
||||||
LaunchedEffect(featureFlagsService) {
|
LaunchedEffect(featureFlagsService) {
|
||||||
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
|
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
|
||||||
enableInRoomCalls = featureFlagsService.isFeatureEnabled(FeatureFlags.InRoomCalls)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(event: MessagesEvents) {
|
fun handleEvents(event: MessagesEvents) {
|
||||||
|
|
@ -201,6 +204,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
customReactionState = customReactionState,
|
customReactionState = customReactionState,
|
||||||
reactionSummaryState = reactionSummaryState,
|
reactionSummaryState = reactionSummaryState,
|
||||||
retrySendMenuState = retryState,
|
retrySendMenuState = retryState,
|
||||||
|
readReceiptBottomSheetState = readReceiptBottomSheetState,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
showReinvitePrompt = showReinvitePrompt,
|
showReinvitePrompt = showReinvitePrompt,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
@ -43,6 +44,7 @@ data class MessagesState(
|
||||||
val customReactionState: CustomReactionState,
|
val customReactionState: CustomReactionState,
|
||||||
val reactionSummaryState: ReactionSummaryState,
|
val reactionSummaryState: ReactionSummaryState,
|
||||||
val retrySendMenuState: RetrySendMenuState,
|
val retrySendMenuState: RetrySendMenuState,
|
||||||
|
val readReceiptBottomSheetState: ReadReceiptBottomSheetState,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val inviteProgress: Async<Unit>,
|
val inviteProgress: Async<Unit>,
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -65,7 +67,14 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
),
|
),
|
||||||
aMessagesState().copy(
|
aMessagesState().copy(
|
||||||
isCallOngoing = true,
|
isCallOngoing = true,
|
||||||
)
|
),
|
||||||
|
aMessagesState().copy(
|
||||||
|
enableVoiceMessages = true,
|
||||||
|
voiceMessageComposerState = aVoiceMessageComposerState(
|
||||||
|
voiceMessageState = aVoiceMessagePreviewState(),
|
||||||
|
showSendFailureDialog = true
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +97,10 @@ fun aMessagesState() = MessagesState(
|
||||||
selectedEvent = null,
|
selectedEvent = null,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
),
|
),
|
||||||
|
readReceiptBottomSheetState = ReadReceiptBottomSheetState(
|
||||||
|
selectedEvent = null,
|
||||||
|
eventSink = {},
|
||||||
|
),
|
||||||
actionListState = anActionListState(),
|
actionListState = anActionListState(),
|
||||||
customReactionState = CustomReactionState(
|
customReactionState = CustomReactionState(
|
||||||
target = CustomReactionState.Target.None,
|
target = CustomReactionState.Target.None,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -70,11 +71,14 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
|
||||||
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||||
|
|
@ -211,6 +215,9 @@ fun MessagesView(
|
||||||
onReactionClicked = ::onEmojiReactionClicked,
|
onReactionClicked = ::onEmojiReactionClicked,
|
||||||
onReactionLongClicked = ::onEmojiReactionLongClicked,
|
onReactionLongClicked = ::onEmojiReactionLongClicked,
|
||||||
onMoreReactionsClicked = ::onMoreReactionsClicked,
|
onMoreReactionsClicked = ::onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = { event ->
|
||||||
|
state.readReceiptBottomSheetState.eventSink(ReadReceiptBottomSheetEvents.EventSelected(event))
|
||||||
|
},
|
||||||
onSendLocationClicked = onSendLocationClicked,
|
onSendLocationClicked = onSendLocationClicked,
|
||||||
onCreatePollClicked = onCreatePollClicked,
|
onCreatePollClicked = onCreatePollClicked,
|
||||||
onSwipeToReply = { targetEvent ->
|
onSwipeToReply = { targetEvent ->
|
||||||
|
|
@ -245,13 +252,12 @@ fun MessagesView(
|
||||||
)
|
)
|
||||||
|
|
||||||
ReactionSummaryView(state = state.reactionSummaryState)
|
ReactionSummaryView(state = state.reactionSummaryState)
|
||||||
RetrySendMessageMenu(
|
RetrySendMessageMenu(state = state.retrySendMenuState)
|
||||||
state = state.retrySendMenuState
|
ReadReceiptBottomSheet(
|
||||||
)
|
state = state.readReceiptBottomSheetState,
|
||||||
|
onUserDataClicked = onUserDataClicked,
|
||||||
ReinviteDialog(
|
|
||||||
state = state
|
|
||||||
)
|
)
|
||||||
|
ReinviteDialog(state = state)
|
||||||
|
|
||||||
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
||||||
// We need to hide the keyboard automatically when navigating out of this screen.
|
// We need to hide the keyboard automatically when navigating out of this screen.
|
||||||
|
|
@ -309,6 +315,7 @@ private fun MessagesViewContent(
|
||||||
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onSendLocationClicked: () -> Unit,
|
onSendLocationClicked: () -> Unit,
|
||||||
|
|
@ -340,15 +347,20 @@ private fun MessagesViewContent(
|
||||||
appName = state.appName
|
appName = state.appName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.enableVoiceMessages && state.voiceMessageComposerState.showSendFailureDialog) {
|
||||||
|
VoiceMessageSendingFailedDialog(
|
||||||
|
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This key is used to force the sheet to be remeasured when the content changes.
|
// This key is used to force the sheet to be remeasured when the content changes.
|
||||||
// Any state change that should trigger a height size should be added to the list of remembered values here.
|
// Any state change that should trigger a height size should be added to the list of remembered values here.
|
||||||
val sheetResizeContentKey = remember(
|
val sheetResizeContentKey = remember { mutableIntStateOf(0) }
|
||||||
state.composerState.mode.relatedEventId,
|
LaunchedEffect(
|
||||||
state.composerState.richTextEditorState.lineCount,
|
state.composerState.richTextEditorState.lineCount,
|
||||||
state.composerState.memberSuggestions.size
|
state.composerState.showTextFormatting,
|
||||||
) {
|
) {
|
||||||
Random.nextInt()
|
sheetResizeContentKey.intValue = Random.nextInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpandableBottomSheetScaffold(
|
ExpandableBottomSheetScaffold(
|
||||||
|
|
@ -367,6 +379,7 @@ private fun MessagesViewContent(
|
||||||
TimelineView(
|
TimelineView(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
|
roomName = state.roomName.dataOrNull(),
|
||||||
onMessageClicked = onMessageClicked,
|
onMessageClicked = onMessageClicked,
|
||||||
onMessageLongClicked = onMessageLongClicked,
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
|
|
@ -374,6 +387,7 @@ private fun MessagesViewContent(
|
||||||
onReactionClicked = onReactionClicked,
|
onReactionClicked = onReactionClicked,
|
||||||
onReactionLongClicked = onReactionLongClicked,
|
onReactionLongClicked = onReactionLongClicked,
|
||||||
onMoreReactionsClicked = onMoreReactionsClicked,
|
onMoreReactionsClicked = onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -383,7 +397,7 @@ private fun MessagesViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
sheetContentKey = sheetResizeContentKey,
|
sheetContentKey = sheetResizeContentKey.intValue,
|
||||||
sheetTonalElevation = 0.dp,
|
sheetTonalElevation = 0.dp,
|
||||||
sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp,
|
sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp,
|
||||||
)
|
)
|
||||||
|
|
@ -399,7 +413,8 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
if (state.userHasPermissionToSendMessage) {
|
if (state.userHasPermissionToSendMessage) {
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
MentionSuggestionsPickerView(
|
MentionSuggestionsPickerView(
|
||||||
modifier = Modifier.heightIn(max = 230.dp)
|
modifier = Modifier
|
||||||
|
.heightIn(max = 230.dp)
|
||||||
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
||||||
.nestedScroll(object : NestedScrollConnection {
|
.nestedScroll(object : NestedScrollConnection {
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
|
@ -411,7 +426,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
roomAvatarData = state.roomAvatar.dataOrNull(),
|
||||||
memberSuggestions = state.composerState.memberSuggestions,
|
memberSuggestions = state.composerState.memberSuggestions,
|
||||||
onSuggestionSelected = {
|
onSuggestionSelected = {
|
||||||
// TODO pass the selected suggestion to the RTE so it can be inserted as a pill
|
state.composerState.eventSink(MessageComposerEvents.InsertMention(it))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
MessageComposerView(
|
MessageComposerView(
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
|
@ -33,9 +35,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.ListItem
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -70,12 +69,17 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatterImpl
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatterImpl
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.toSp
|
import io.element.android.libraries.designsystem.text.toSp
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.hide
|
import io.element.android.libraries.designsystem.theme.components.hide
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
|
|
@ -140,15 +144,13 @@ fun ActionListView(
|
||||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||||
onCustomReactionClicked = ::onCustomReactionClicked,
|
onCustomReactionClicked = ::onCustomReactionClicked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 32.dp)
|
.navigationBarsPadding()
|
||||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
.imePadding()
|
||||||
// .imePadding()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SheetContent(
|
private fun SheetContent(
|
||||||
state: ActionListState,
|
state: ActionListState,
|
||||||
|
|
@ -198,18 +200,13 @@ private fun SheetContent(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onActionClicked(action)
|
onActionClicked(action)
|
||||||
},
|
},
|
||||||
text = {
|
headlineContent = {
|
||||||
Text(
|
Text(text = stringResource(id = action.titleRes))
|
||||||
text = stringResource(id = action.titleRes),
|
|
||||||
color = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
icon = {
|
leadingContent = ListItemContent.Icon(IconSource.Resource(action.icon)),
|
||||||
Icon(
|
style = when {
|
||||||
resourceId = action.icon,
|
action.destructive -> ListItemStyle.Destructive
|
||||||
contentDescription = "",
|
else -> ListItemStyle.Primary
|
||||||
tint = if (action.destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +370,7 @@ private fun EmojiReactionsRow(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
resourceId = CommonDrawables.ic_september_add_reaction,
|
resourceId = CommonDrawables.ic_add_reaction,
|
||||||
contentDescription = "Emojis",
|
contentDescription = "Emojis",
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -410,8 +407,7 @@ private fun EmojiButton(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
emoji,
|
emoji,
|
||||||
fontSize = 24.dp.toSp(),
|
style = ElementTheme.typography.fontBodyLgRegular.copy(fontSize = 24.dp.toSp(), color = Color.White),
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,13 @@ sealed class TimelineItemAction(
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int,
|
||||||
val destructive: Boolean = false
|
val destructive: Boolean = false
|
||||||
) {
|
) {
|
||||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, CommonDrawables.ic_september_forward)
|
data object Forward : TimelineItemAction(CommonStrings.action_forward, CommonDrawables.ic_forward)
|
||||||
data object Copy : TimelineItemAction(CommonStrings.action_copy, CommonDrawables.ic_september_copy)
|
data object Copy : TimelineItemAction(CommonStrings.action_copy, CommonDrawables.ic_copy)
|
||||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, CommonDrawables.ic_compound_delete, destructive = true)
|
data object Redact : TimelineItemAction(CommonStrings.action_remove, CommonDrawables.ic_compound_delete, destructive = true)
|
||||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CommonDrawables.ic_september_reply)
|
data object Reply : TimelineItemAction(CommonStrings.action_reply, CommonDrawables.ic_reply)
|
||||||
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CommonDrawables.ic_september_reply)
|
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CommonDrawables.ic_reply)
|
||||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, CommonDrawables.ic_september_edit_outline)
|
data object Edit : TimelineItemAction(CommonStrings.action_edit, CommonDrawables.ic_edit_outline)
|
||||||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_september_view_source)
|
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
||||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CommonDrawables.ic_compound_chat_problem, destructive = true)
|
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CommonDrawables.ic_compound_chat_problem, destructive = true)
|
||||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CommonDrawables.ic_poll_end)
|
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CommonDrawables.ic_poll_end)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ private fun MediaFileView(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isAudio) Icons.Outlined.GraphicEq else null,
|
imageVector = if (isAudio) Icons.Outlined.GraphicEq else null,
|
||||||
resourceId = if (isAudio) null else CommonDrawables.ic_september_attachment,
|
resourceId = if (isAudio) null else CommonDrawables.ic_attachment,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.background,
|
tint = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.mentions
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface MentionSuggestion {
|
||||||
|
data object Room : MentionSuggestion
|
||||||
|
data class Member(val roomMember: RoomMember) : MentionSuggestion
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -52,8 +51,8 @@ fun MentionSuggestionsPickerView(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatarData: AvatarData?,
|
roomAvatarData: AvatarData?,
|
||||||
memberSuggestions: ImmutableList<RoomMemberSuggestion>,
|
memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||||
onSuggestionSelected: (RoomMemberSuggestion) -> Unit,
|
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -63,8 +62,8 @@ fun MentionSuggestionsPickerView(
|
||||||
memberSuggestions,
|
memberSuggestions,
|
||||||
key = { suggestion ->
|
key = { suggestion ->
|
||||||
when (suggestion) {
|
when (suggestion) {
|
||||||
is RoomMemberSuggestion.Room -> "@room"
|
is MentionSuggestion.Room -> "@room"
|
||||||
is RoomMemberSuggestion.Member -> suggestion.roomMember.userId.value
|
is MentionSuggestion.Member -> suggestion.roomMember.userId.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -85,18 +84,18 @@ fun MentionSuggestionsPickerView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomMemberSuggestionItemView(
|
private fun RoomMemberSuggestionItemView(
|
||||||
memberSuggestion: RoomMemberSuggestion,
|
memberSuggestion: MentionSuggestion,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData?,
|
roomAvatar: AvatarData?,
|
||||||
onSuggestionSelected: (RoomMemberSuggestion) -> Unit,
|
onSuggestionSelected: (MentionSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
val avatarSize = AvatarSize.TimelineRoom
|
val avatarSize = AvatarSize.TimelineRoom
|
||||||
val avatarData = when (memberSuggestion) {
|
val avatarData = when (memberSuggestion) {
|
||||||
is RoomMemberSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
is MentionSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
||||||
is RoomMemberSuggestion.Member -> AvatarData(
|
is MentionSuggestion.Member -> AvatarData(
|
||||||
memberSuggestion.roomMember.userId.value,
|
memberSuggestion.roomMember.userId.value,
|
||||||
memberSuggestion.roomMember.displayName,
|
memberSuggestion.roomMember.displayName,
|
||||||
memberSuggestion.roomMember.avatarUrl,
|
memberSuggestion.roomMember.avatarUrl,
|
||||||
|
|
@ -104,13 +103,13 @@ private fun RoomMemberSuggestionItemView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val title = when (memberSuggestion) {
|
val title = when (memberSuggestion) {
|
||||||
is RoomMemberSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
|
is MentionSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||||
is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.displayName
|
is MentionSuggestion.Member -> memberSuggestion.roomMember.displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
val subtitle = when (memberSuggestion) {
|
val subtitle = when (memberSuggestion) {
|
||||||
is RoomMemberSuggestion.Room -> "@room"
|
is MentionSuggestion.Room -> "@room"
|
||||||
is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
is MentionSuggestion.Member -> memberSuggestion.roomMember.userId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp))
|
||||||
|
|
@ -159,9 +158,9 @@ internal fun MentionSuggestionsPickerView_Preview() {
|
||||||
roomName = "Room",
|
roomName = "Room",
|
||||||
roomAvatarData = null,
|
roomAvatarData = null,
|
||||||
memberSuggestions = persistentListOf(
|
memberSuggestions = persistentListOf(
|
||||||
RoomMemberSuggestion.Room,
|
MentionSuggestion.Room,
|
||||||
RoomMemberSuggestion.Member(roomMember),
|
MentionSuggestion.Member(roomMember),
|
||||||
RoomMemberSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
MentionSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||||
),
|
),
|
||||||
onSuggestionSelected = {}
|
onSuggestionSelected = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.mentions
|
package io.element.android.features.messages.impl.mentions
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion
|
import io.element.android.libraries.core.data.filterUpTo
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
|
@ -46,10 +46,8 @@ object MentionSuggestionsProcessor {
|
||||||
roomMembersState: MatrixRoomMembersState,
|
roomMembersState: MatrixRoomMembersState,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
canSendRoomMention: suspend () -> Boolean,
|
canSendRoomMention: suspend () -> Boolean,
|
||||||
): List<RoomMemberSuggestion> {
|
): List<MentionSuggestion> {
|
||||||
val members = roomMembersState.roomMembers()
|
val members = roomMembersState.roomMembers()
|
||||||
// Take the first MAX_BATCH_ITEMS only
|
|
||||||
?.take(MAX_BATCH_ITEMS)
|
|
||||||
return when {
|
return when {
|
||||||
members.isNullOrEmpty() || suggestion == null -> {
|
members.isNullOrEmpty() || suggestion == null -> {
|
||||||
// Clear suggestions
|
// Clear suggestions
|
||||||
|
|
@ -61,7 +59,7 @@ object MentionSuggestionsProcessor {
|
||||||
// Replace suggestions
|
// Replace suggestions
|
||||||
val matchingMembers = getMemberSuggestions(
|
val matchingMembers = getMemberSuggestions(
|
||||||
query = suggestion.text,
|
query = suggestion.text,
|
||||||
roomMembers = roomMembersState.roomMembers(),
|
roomMembers = members,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
canSendRoomMention = canSendRoomMention()
|
canSendRoomMention = canSendRoomMention()
|
||||||
)
|
)
|
||||||
|
|
@ -81,7 +79,7 @@ object MentionSuggestionsProcessor {
|
||||||
roomMembers: List<RoomMember>?,
|
roomMembers: List<RoomMember>?,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
canSendRoomMention: Boolean,
|
canSendRoomMention: Boolean,
|
||||||
): List<RoomMemberSuggestion> {
|
): List<MentionSuggestion> {
|
||||||
return if (roomMembers.isNullOrEmpty()) {
|
return if (roomMembers.isNullOrEmpty()) {
|
||||||
emptyList()
|
emptyList()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -95,14 +93,14 @@ object MentionSuggestionsProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
val matchingMembers = roomMembers
|
val matchingMembers = roomMembers
|
||||||
// Search only in joined members, exclude the current user
|
// Search only in joined members, up to MAX_BATCH_ITEMS, exclude the current user
|
||||||
.filter { member ->
|
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
||||||
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
||||||
}
|
}
|
||||||
.map(RoomMemberSuggestion::Member)
|
.map(MentionSuggestion::Member)
|
||||||
|
|
||||||
if ("room".contains(query) && canSendRoomMention) {
|
if ("room".contains(query) && canSendRoomMention) {
|
||||||
listOf(RoomMemberSuggestion.Room) + matchingMembers
|
listOf(MentionSuggestion.Room) + matchingMembers
|
||||||
} else {
|
} else {
|
||||||
matchingMembers
|
matchingMembers
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,8 @@ package io.element.android.features.messages.impl.messagecomposer
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.material.ListItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -33,12 +32,14 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconSource
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
|
||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
|
@ -93,7 +94,6 @@ internal fun AttachmentsBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentSourcePickerMenu(
|
private fun AttachmentSourcePickerMenu(
|
||||||
state: MessageComposerState,
|
state: MessageComposerState,
|
||||||
|
|
@ -103,28 +103,33 @@ private fun AttachmentSourcePickerMenu(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier.padding(bottom = 32.dp)
|
modifier = modifier
|
||||||
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) },
|
||||||
icon = { Icon(CommonDrawables.ic_september_photo_video_library, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_image)),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) },
|
||||||
icon = { Icon(CommonDrawables.ic_september_attachment, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_attachment)),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) },
|
||||||
icon = { Icon(CommonDrawables.ic_september_take_photo_camera, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_take_photo_camera, )),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) },
|
||||||
icon = { Icon(CommonDrawables.ic_september_video_call, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_video_call)),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
if (state.canShareLocation) {
|
if (state.canShareLocation) {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|
@ -132,8 +137,9 @@ private fun AttachmentSourcePickerMenu(
|
||||||
state.eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
state.eventSink(MessageComposerEvents.PickAttachmentSource.Location)
|
||||||
onSendLocationClicked()
|
onSendLocationClicked()
|
||||||
},
|
},
|
||||||
icon = { Icon(CommonDrawables.ic_september_location, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_location_pin) ),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.canCreatePoll) {
|
if (state.canCreatePoll) {
|
||||||
|
|
@ -142,15 +148,17 @@ private fun AttachmentSourcePickerMenu(
|
||||||
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
|
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
|
||||||
onCreatePollClicked()
|
onCreatePollClicked()
|
||||||
},
|
},
|
||||||
icon = { Icon(CommonDrawables.ic_compound_polls, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_compound_polls)),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (enableTextFormatting) {
|
if (enableTextFormatting) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
|
||||||
icon = { Icon(CommonDrawables.ic_september_text_formatting, null) },
|
leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_text_formatting, null)),
|
||||||
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
|
||||||
|
style = ListItemStyle.Primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.Message
|
import io.element.android.libraries.textcomposer.model.Message
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
|
|
@ -41,4 +42,5 @@ sealed interface MessageComposerEvents {
|
||||||
data object CancelSendAttachment : MessageComposerEvents
|
data object CancelSendAttachment : MessageComposerEvents
|
||||||
data class Error(val error: Throwable) : MessageComposerEvents
|
data class Error(val error: Throwable) : MessageComposerEvents
|
||||||
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
|
||||||
|
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -36,6 +35,7 @@ import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||||
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
|
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -45,8 +45,9 @@ import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -67,6 +68,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -87,7 +90,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
private val messageComposerContext: MessageComposerContextImpl,
|
private val messageComposerContext: MessageComposerContextImpl,
|
||||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<MessageComposerState> {
|
) : Presenter<MessageComposerState> {
|
||||||
|
|
||||||
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
|
||||||
|
|
@ -173,7 +176,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val memberSuggestions = remember { mutableStateListOf<RoomMemberSuggestion>() }
|
val memberSuggestions = remember { mutableStateListOf<MentionSuggestion>() }
|
||||||
LaunchedEffect(isMentionsEnabled) {
|
LaunchedEffect(isMentionsEnabled) {
|
||||||
if (!isMentionsEnabled) return@LaunchedEffect
|
if (!isMentionsEnabled) return@LaunchedEffect
|
||||||
val currentUserId = currentSessionIdHolder.current
|
val currentUserId = currentSessionIdHolder.current
|
||||||
|
|
@ -184,8 +187,11 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
return !roomIsDm && userCanSendAtRoom
|
return !roomIsDm && userCanSendAtRoom
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestionSearchTrigger
|
// This will trigger a search immediately when `@` is typed
|
||||||
.debounce(0.5.seconds)
|
val mentionStartTrigger = suggestionSearchTrigger.filter { it?.text.isNullOrEmpty() }
|
||||||
|
// This will start a search when the user changes the text after the `@` with a debounce to prevent too much wasted work
|
||||||
|
val mentionCompletionTrigger = suggestionSearchTrigger.filter { !it?.text.isNullOrEmpty() }.debounce(0.3.seconds)
|
||||||
|
merge(mentionStartTrigger, mentionCompletionTrigger)
|
||||||
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
|
.combine(room.membersStateFlow) { suggestion, roomMembersState ->
|
||||||
memberSuggestions.clear()
|
memberSuggestions.clear()
|
||||||
val result = MentionSuggestionsProcessor.process(
|
val result = MentionSuggestionsProcessor.process(
|
||||||
|
|
@ -284,6 +290,20 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
is MessageComposerEvents.SuggestionReceived -> {
|
is MessageComposerEvents.SuggestionReceived -> {
|
||||||
suggestionSearchTrigger.value = event.suggestion
|
suggestionSearchTrigger.value = event.suggestion
|
||||||
}
|
}
|
||||||
|
is MessageComposerEvents.InsertMention -> {
|
||||||
|
localCoroutineScope.launch {
|
||||||
|
when (val mention = event.mention) {
|
||||||
|
is MentionSuggestion.Room -> {
|
||||||
|
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||||
|
}
|
||||||
|
is MentionSuggestion.Member -> {
|
||||||
|
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
||||||
|
val link = PermalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||||
|
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,6 +317,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
canCreatePoll = canCreatePoll.value,
|
canCreatePoll = canCreatePoll.value,
|
||||||
attachmentsState = attachmentsState.value,
|
attachmentsState = attachmentsState.value,
|
||||||
memberSuggestions = memberSuggestions.toPersistentList(),
|
memberSuggestions = memberSuggestions.toPersistentList(),
|
||||||
|
currentUserId = currentSessionIdHolder.current,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -307,15 +328,25 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
richTextEditorState: RichTextEditorState,
|
richTextEditorState: RichTextEditorState,
|
||||||
) = launch {
|
) = launch {
|
||||||
val capturedMode = messageComposerContext.composerMode
|
val capturedMode = messageComposerContext.composerMode
|
||||||
|
val mentions = richTextEditorState.mentionsState?.let { state ->
|
||||||
|
buildList {
|
||||||
|
if (state.hasAtRoomMention) {
|
||||||
|
add(Mention.AtRoom)
|
||||||
|
}
|
||||||
|
for (userId in state.userIds) {
|
||||||
|
add(Mention.User(userId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.orEmpty()
|
||||||
// Reset composer right away
|
// Reset composer right away
|
||||||
richTextEditorState.setHtml("")
|
richTextEditorState.setHtml("")
|
||||||
updateComposerMode(MessageComposerMode.Normal)
|
updateComposerMode(MessageComposerMode.Normal)
|
||||||
when (capturedMode) {
|
when (capturedMode) {
|
||||||
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html)
|
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = mentions)
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
val eventId = capturedMode.eventId
|
val eventId = capturedMode.eventId
|
||||||
val transactionId = capturedMode.transactionId
|
val transactionId = capturedMode.transactionId
|
||||||
room.editMessage(eventId, transactionId, message.markdown, message.html)
|
room.editMessage(eventId, transactionId, message.markdown, message.html, mentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageComposerMode.Quote -> TODO()
|
is MessageComposerMode.Quote -> TODO()
|
||||||
|
|
@ -323,6 +354,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
capturedMode.eventId,
|
capturedMode.eventId,
|
||||||
message.markdown,
|
message.markdown,
|
||||||
message.html,
|
message.html,
|
||||||
|
mentions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
analyticsService.capture(
|
analyticsService.capture(
|
||||||
|
|
@ -410,8 +442,3 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
|
||||||
sealed interface RoomMemberSuggestion {
|
|
||||||
data object Room : RoomMemberSuggestion
|
|
||||||
data class Member(val roomMember: RoomMember) : RoomMemberSuggestion
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ package io.element.android.features.messages.impl.messagecomposer
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -33,7 +35,8 @@ data class MessageComposerState(
|
||||||
val canShareLocation: Boolean,
|
val canShareLocation: Boolean,
|
||||||
val canCreatePoll: Boolean,
|
val canCreatePoll: Boolean,
|
||||||
val attachmentsState: AttachmentsState,
|
val attachmentsState: AttachmentsState,
|
||||||
val memberSuggestions: ImmutableList<RoomMemberSuggestion>,
|
val memberSuggestions: ImmutableList<MentionSuggestion>,
|
||||||
|
val currentUserId: UserId,
|
||||||
val eventSink: (MessageComposerEvents) -> Unit,
|
val eventSink: (MessageComposerEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasFocus: Boolean = richTextEditorState.hasFocus
|
val hasFocus: Boolean = richTextEditorState.hasFocus
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.messages.impl.mentions.MentionSuggestion
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -38,7 +40,7 @@ fun aMessageComposerState(
|
||||||
canShareLocation: Boolean = true,
|
canShareLocation: Boolean = true,
|
||||||
canCreatePoll: Boolean = true,
|
canCreatePoll: Boolean = true,
|
||||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||||
memberSuggestions: ImmutableList<RoomMemberSuggestion> = persistentListOf(),
|
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
|
||||||
) = MessageComposerState(
|
) = MessageComposerState(
|
||||||
richTextEditorState = composerState,
|
richTextEditorState = composerState,
|
||||||
isFullScreen = isFullScreen,
|
isFullScreen = isFullScreen,
|
||||||
|
|
@ -49,5 +51,6 @@ fun aMessageComposerState(
|
||||||
canCreatePoll = canCreatePoll,
|
canCreatePoll = canCreatePoll,
|
||||||
attachmentsState = attachmentsState,
|
attachmentsState = attachmentsState,
|
||||||
memberSuggestions = memberSuggestions,
|
memberSuggestions = memberSuggestions,
|
||||||
|
currentUserId = UserId("@alice:localhost"),
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
|
||||||
|
|
@ -32,9 +33,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.textcomposer.TextComposer
|
import io.element.android.libraries.textcomposer.TextComposer
|
||||||
import io.element.android.libraries.textcomposer.model.Message
|
import io.element.android.libraries.textcomposer.model.Message
|
||||||
import io.element.android.libraries.textcomposer.model.PressEvent
|
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
import io.element.android.libraries.textcomposer.model.Suggestion
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
|
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -46,6 +47,7 @@ internal fun MessageComposerView(
|
||||||
enableVoiceMessages: Boolean,
|
enableVoiceMessages: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
fun sendMessage(message: Message) {
|
fun sendMessage(message: Message) {
|
||||||
state.eventSink(MessageComposerEvents.SendMessage(message))
|
state.eventSink(MessageComposerEvents.SendMessage(message))
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +61,7 @@ internal fun MessageComposerView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDismissTextFormatting() {
|
fun onDismissTextFormatting() {
|
||||||
|
view.clearFocus()
|
||||||
state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false))
|
state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,8 +80,8 @@ internal fun MessageComposerView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onVoiceRecordButtonEvent = { press: PressEvent ->
|
val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent ->
|
||||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press))
|
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecorderEvent(press))
|
||||||
}
|
}
|
||||||
|
|
||||||
val onSendVoiceMessage = {
|
val onSendVoiceMessage = {
|
||||||
|
|
@ -107,12 +110,13 @@ internal fun MessageComposerView(
|
||||||
onDismissTextFormatting = ::onDismissTextFormatting,
|
onDismissTextFormatting = ::onDismissTextFormatting,
|
||||||
enableTextFormatting = enableTextFormatting,
|
enableTextFormatting = enableTextFormatting,
|
||||||
enableVoiceMessages = enableVoiceMessages,
|
enableVoiceMessages = enableVoiceMessages,
|
||||||
onVoiceRecordButtonEvent = onVoiceRecordButtonEvent,
|
onVoiceRecorderEvent = onVoiceRecorderEvent,
|
||||||
onVoicePlayerEvent = onVoicePlayerEvent,
|
onVoicePlayerEvent = onVoicePlayerEvent,
|
||||||
onSendVoiceMessage = onSendVoiceMessage,
|
onSendVoiceMessage = onSendVoiceMessage,
|
||||||
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
onDeleteVoiceMessage = onDeleteVoiceMessage,
|
||||||
onSuggestionReceived = ::onSuggestionReceived,
|
onSuggestionReceived = ::onSuggestionReceived,
|
||||||
onError = ::onError,
|
onError = ::onError,
|
||||||
|
currentUserId = state.currentUserId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncView
|
import io.element.android.libraries.designsystem.components.async.AsyncView
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
|
@ -101,14 +102,14 @@ fun ReportMessageView(
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.reason,
|
value = state.reason,
|
||||||
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
|
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) },
|
||||||
placeholder = { Text(stringResource(CommonStrings.report_content_hint)) },
|
placeholder = { Text(stringResource(R.string.report_content_hint)) },
|
||||||
enabled = !isSending,
|
enabled = !isSending,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 90.dp)
|
.heightIn(min = 90.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(CommonStrings.report_content_explanation),
|
text = stringResource(R.string.report_content_explanation),
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
|
@ -122,11 +123,11 @@ fun ReportMessageView(
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(CommonStrings.screen_report_content_block_user),
|
text = stringResource(R.string.screen_report_content_block_user),
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(CommonStrings.screen_report_content_block_user_hint),
|
text = stringResource(R.string.screen_report_content_block_user_hint),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,17 @@ import im.vector.app.features.analytics.plan.PollVote
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
|
|
@ -63,6 +67,8 @@ class TimelinePresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val verificationService: SessionVerificationService,
|
private val verificationService: SessionVerificationService,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
|
|
||||||
private val timeline = room.timeline
|
private val timeline = room.timeline
|
||||||
|
|
@ -97,6 +103,9 @@ class TimelinePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val readReceiptsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.ReadReceipts).collectAsState(initial = false)
|
||||||
|
val membersState by room.membersStateFlow.collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
TimelineEvents.LoadMore -> localScope.paginateBackwards()
|
||||||
|
|
@ -136,12 +145,23 @@ class TimelinePresenter @Inject constructor(
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
timeline
|
timeline
|
||||||
.timelineItems
|
.timelineItems
|
||||||
.onEach(timelineItemsFactory::replaceWith)
|
.onEach {
|
||||||
|
timelineItemsFactory.replaceWith(
|
||||||
|
timelineItems = it,
|
||||||
|
roomMembers = if (readReceiptsEnabled) {
|
||||||
|
membersState.roomMembers().orEmpty()
|
||||||
|
} else {
|
||||||
|
// Give an empty list to not affect performance
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.onEach { timelineItems ->
|
.onEach { timelineItems ->
|
||||||
if (timelineItems.isEmpty()) {
|
if (timelineItems.isEmpty()) {
|
||||||
paginateBackwards()
|
paginateBackwards()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,6 +170,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
paginationState = paginationState,
|
paginationState = paginationState,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
|
showReadReceipts = readReceiptsEnabled,
|
||||||
hasNewItems = hasNewItems.value,
|
hasNewItems = hasNewItems.value,
|
||||||
sessionState = sessionState,
|
sessionState = sessionState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TimelineState(
|
data class TimelineState(
|
||||||
val timelineItems: ImmutableList<TimelineItem>,
|
val timelineItems: ImmutableList<TimelineItem>,
|
||||||
|
val showReadReceipts: Boolean,
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||||
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
|
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
|
|
@ -43,7 +45,12 @@ import kotlin.random.Random
|
||||||
|
|
||||||
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
|
showReadReceipts = false,
|
||||||
|
paginationState = MatrixTimeline.PaginationState(
|
||||||
|
isBackPaginating = false,
|
||||||
|
hasMoreToLoadBackwards = true,
|
||||||
|
beginningOfRoomReached = false,
|
||||||
|
),
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
userHasPermissionToSendMessage = true,
|
userHasPermissionToSendMessage = true,
|
||||||
hasNewItems = false,
|
hasNewItems = false,
|
||||||
|
|
@ -114,11 +121,12 @@ internal fun aTimelineItemEvent(
|
||||||
senderDisplayName: String = "Sender",
|
senderDisplayName: String = "Sender",
|
||||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
sendState: LocalEventSendState = LocalEventSendState.Sent(eventId),
|
sendState: LocalEventSendState? = null,
|
||||||
inReplyTo: InReplyTo? = null,
|
inReplyTo: InReplyTo? = null,
|
||||||
isThreaded: Boolean = false,
|
isThreaded: Boolean = false,
|
||||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||||
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||||
|
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = UUID.randomUUID().toString(),
|
id = UUID.randomUUID().toString(),
|
||||||
|
|
@ -128,6 +136,7 @@ internal fun aTimelineItemEvent(
|
||||||
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
||||||
content = content,
|
content = content,
|
||||||
reactionsState = timelineItemReactions,
|
reactionsState = timelineItemReactions,
|
||||||
|
readReceiptState = readReceiptState,
|
||||||
sentTime = "12:34",
|
sentTime = "12:34",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
senderDisplayName = senderDisplayName,
|
senderDisplayName = senderDisplayName,
|
||||||
|
|
@ -169,6 +178,12 @@ internal fun aTimelineItemDebugInfo(
|
||||||
model, originalJson, latestEditedJson
|
model, originalJson, latestEditedJson
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts {
|
||||||
|
return TimelineItemReadReceipts(
|
||||||
|
receipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
|
fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
|
||||||
val event = aTimelineItemEvent(
|
val event = aTimelineItemEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ import io.element.android.features.messages.impl.timeline.components.TimelineIte
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemStateEventRow
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemStateEventRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
|
|
@ -82,6 +83,7 @@ import kotlinx.coroutines.launch
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineView(
|
fun TimelineView(
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
|
roomName: String?,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -90,6 +92,7 @@ fun TimelineView(
|
||||||
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionLongClicked: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onReachedLoadMore() {
|
fun onReachedLoadMore() {
|
||||||
|
|
@ -124,6 +127,9 @@ fun TimelineView(
|
||||||
) { timelineItem ->
|
) { timelineItem ->
|
||||||
TimelineItemRow(
|
TimelineItemRow(
|
||||||
timelineItem = timelineItem,
|
timelineItem = timelineItem,
|
||||||
|
showReadReceipts = state.showReadReceipts,
|
||||||
|
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true
|
||||||
|
&& state.timelineItems.first().identifier() == timelineItem.identifier(),
|
||||||
highlightedItem = state.highlightedEventId?.value,
|
highlightedItem = state.highlightedEventId?.value,
|
||||||
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
|
||||||
onClick = onMessageClicked,
|
onClick = onMessageClicked,
|
||||||
|
|
@ -133,6 +139,7 @@ fun TimelineView(
|
||||||
onReactionClick = onReactionClicked,
|
onReactionClick = onReactionClicked,
|
||||||
onReactionLongClick = onReactionLongClicked,
|
onReactionLongClick = onReactionLongClicked,
|
||||||
onMoreReactionsClick = onMoreReactionsClicked,
|
onMoreReactionsClick = onMoreReactionsClicked,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
sessionState = state.sessionState,
|
sessionState = state.sessionState,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
|
|
@ -148,6 +155,11 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state.paginationState.beginningOfRoomReached) {
|
||||||
|
item(contentType = "BeginningOfRoomReached") {
|
||||||
|
TimelineItemRoomBeginningView(roomName = roomName)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
|
|
@ -162,6 +174,8 @@ fun TimelineView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimelineItemRow(
|
private fun TimelineItemRow(
|
||||||
timelineItem: TimelineItem,
|
timelineItem: TimelineItem,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
highlightedItem: String?,
|
highlightedItem: String?,
|
||||||
userHasPermissionToSendMessage: Boolean,
|
userHasPermissionToSendMessage: Boolean,
|
||||||
sessionState: SessionState,
|
sessionState: SessionState,
|
||||||
|
|
@ -172,6 +186,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
|
@ -198,6 +213,8 @@ private fun TimelineItemRow(
|
||||||
} else {
|
} else {
|
||||||
TimelineItemEventRow(
|
TimelineItemEventRow(
|
||||||
event = timelineItem,
|
event = timelineItem,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
||||||
onClick = { onClick(timelineItem) },
|
onClick = { onClick(timelineItem) },
|
||||||
|
|
@ -207,6 +224,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
@ -237,6 +255,8 @@ private fun TimelineItemRow(
|
||||||
timelineItem.events.forEach { subGroupEvent ->
|
timelineItem.events.forEach { subGroupEvent ->
|
||||||
TimelineItemRow(
|
TimelineItemRow(
|
||||||
timelineItem = subGroupEvent,
|
timelineItem = subGroupEvent,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
sessionState = sessionState,
|
sessionState = sessionState,
|
||||||
userHasPermissionToSendMessage = false,
|
userHasPermissionToSendMessage = false,
|
||||||
|
|
@ -248,6 +268,7 @@ private fun TimelineItemRow(
|
||||||
onReactionClick = onReactionClick,
|
onReactionClick = onReactionClick,
|
||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
)
|
)
|
||||||
|
|
@ -346,6 +367,7 @@ internal fun TimelineViewPreview(
|
||||||
) {
|
) {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = aTimelineState(timelineItems),
|
state = aTimelineState(timelineItems),
|
||||||
|
roomName = null,
|
||||||
onMessageClicked = {},
|
onMessageClicked = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
|
|
@ -354,6 +376,7 @@ internal fun TimelineViewPreview(
|
||||||
onReactionLongClicked = { _, _ -> },
|
onReactionLongClicked = { _, _ -> },
|
||||||
onMoreReactionsClicked = {},
|
onMoreReactionsClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionP
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessagesAddReactionButtonPreview() = ElementPreview {
|
internal fun MessagesAddReactionButtonPreview() = ElementPreview {
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_september_add_reaction),
|
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {}
|
onLongClick = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ fun RowScope.ReplySwipeIndicator(
|
||||||
alpha = swipeProgress()
|
alpha = swipeProgress()
|
||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
resourceId = CommonDrawables.ic_september_reply,
|
resourceId = CommonDrawables.ic_reply,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||||
|
|
@ -114,6 +116,8 @@ import kotlin.math.roundToInt
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventRow(
|
fun TimelineItemEventRow(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
canReply: Boolean,
|
canReply: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|
@ -124,6 +128,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (event: TimelineItem.Event) -> Unit,
|
||||||
onSwipeToReply: () -> Unit,
|
onSwipeToReply: () -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -173,6 +178,8 @@ fun TimelineItemEventRow(
|
||||||
state = state.draggableState,
|
state = state.draggableState,
|
||||||
),
|
),
|
||||||
event = event,
|
event = event,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -183,6 +190,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
|
onReadReceiptsClicked = { onReadReceiptClick(event) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -190,6 +198,8 @@ fun TimelineItemEventRow(
|
||||||
} else {
|
} else {
|
||||||
TimelineItemEventRowContent(
|
TimelineItemEventRowContent(
|
||||||
event = event,
|
event = event,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -200,6 +210,7 @@ fun TimelineItemEventRow(
|
||||||
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
|
||||||
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
|
||||||
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
onMoreReactionsClicked = { onMoreReactionsClick(event) },
|
||||||
|
onReadReceiptsClicked = { onReadReceiptClick(event) },
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +243,8 @@ private fun SwipeSensitivity(
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimelineItemEventRowContent(
|
private fun TimelineItemEventRowContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|
@ -240,6 +253,7 @@ private fun TimelineItemEventRowContent(
|
||||||
inReplyToClicked: () -> Unit,
|
inReplyToClicked: () -> Unit,
|
||||||
onUserDataClicked: () -> Unit,
|
onUserDataClicked: () -> Unit,
|
||||||
onReactionClicked: (emoji: String) -> Unit,
|
onReactionClicked: (emoji: String) -> Unit,
|
||||||
|
onReadReceiptsClicked: () -> Unit,
|
||||||
onReactionLongClicked: (emoji: String) -> Unit,
|
onReactionLongClicked: (emoji: String) -> Unit,
|
||||||
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
|
@ -256,7 +270,12 @@ private fun TimelineItemEventRowContent(
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
val (sender, message, reactions) = createRefs()
|
val (
|
||||||
|
sender,
|
||||||
|
message,
|
||||||
|
reactions,
|
||||||
|
readReceipts,
|
||||||
|
) = createRefs()
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
val avatarStrokeSize = 3.dp
|
val avatarStrokeSize = 3.dp
|
||||||
|
|
@ -322,6 +341,25 @@ private fun TimelineItemEventRowContent(
|
||||||
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read receipts / Send state
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = ReadReceiptViewState(
|
||||||
|
sendState = event.localSendState,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
receipts = event.readReceiptState.receipts,
|
||||||
|
),
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
onReadReceiptsClicked = onReadReceiptsClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(readReceipts) {
|
||||||
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
|
top.linkTo(reactions.bottom, margin = 4.dp)
|
||||||
|
} else {
|
||||||
|
top.linkTo(message.bottom, margin = 4.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,22 +421,6 @@ private fun MessageEventBubbleContent(
|
||||||
// to its `combinedClickable` parent so we do it manually
|
// to its `combinedClickable` parent so we do it manually
|
||||||
fun onTimestampLongClick() = onMessageLongClick()
|
fun onTimestampLongClick() = onMessageLongClick()
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ContentView(
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
TimelineItemEventContentView(
|
|
||||||
content = event.content,
|
|
||||||
isMine = event.isMine,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onMessageClick,
|
|
||||||
onLongClick = onMessageLongClick,
|
|
||||||
extraPadding = event.toExtraPadding(),
|
|
||||||
eventSink = eventSink,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ThreadDecoration(
|
fun ThreadDecoration(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -422,21 +444,20 @@ private fun MessageEventBubbleContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContentAndTimestampView(
|
fun WithTimestampLayout(
|
||||||
timestampPosition: TimestampPosition,
|
timestampPosition: TimestampPosition,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentModifier: Modifier = Modifier,
|
content: @Composable () -> Unit,
|
||||||
timestampModifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
when (timestampPosition) {
|
when (timestampPosition) {
|
||||||
TimestampPosition.Overlay ->
|
TimestampPosition.Overlay ->
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
ContentView(modifier = contentModifier)
|
content()
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onClick = onTimestampClicked,
|
onClick = onTimestampClicked,
|
||||||
onLongClick = ::onTimestampLongClick,
|
onLongClick = ::onTimestampLongClick,
|
||||||
modifier = timestampModifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||||
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
|
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
|
|
@ -445,24 +466,24 @@ private fun MessageEventBubbleContent(
|
||||||
}
|
}
|
||||||
TimestampPosition.Aligned ->
|
TimestampPosition.Aligned ->
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
ContentView(modifier = contentModifier)
|
content()
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onClick = onTimestampClicked,
|
onClick = onTimestampClicked,
|
||||||
onLongClick = ::onTimestampLongClick,
|
onLongClick = ::onTimestampLongClick,
|
||||||
modifier = timestampModifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TimestampPosition.Below ->
|
TimestampPosition.Below ->
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
ContentView(modifier = contentModifier)
|
content()
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
onClick = onTimestampClicked,
|
onClick = onTimestampClicked,
|
||||||
onLongClick = ::onTimestampLongClick,
|
onLongClick = ::onTimestampLongClick,
|
||||||
modifier = timestampModifier
|
modifier = Modifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -478,52 +499,77 @@ private fun MessageEventBubbleContent(
|
||||||
inReplyToDetails: InReplyTo.Ready?,
|
inReplyToDetails: InReplyTo.Ready?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val modifierWithPadding: Modifier
|
val timestampLayoutModifier: Modifier
|
||||||
val contentModifier: Modifier
|
val contentModifier: Modifier
|
||||||
when {
|
when {
|
||||||
inReplyToDetails != null -> {
|
inReplyToDetails != null -> {
|
||||||
if (timestampPosition == TimestampPosition.Overlay) {
|
if (timestampPosition == TimestampPosition.Overlay) {
|
||||||
modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
timestampLayoutModifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||||
contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
|
contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
|
||||||
} else {
|
} else {
|
||||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||||
modifierWithPadding = Modifier
|
timestampLayoutModifier = Modifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timestampPosition != TimestampPosition.Overlay -> {
|
timestampPosition != TimestampPosition.Overlay -> {
|
||||||
modifierWithPadding = Modifier
|
timestampLayoutModifier = Modifier
|
||||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
modifierWithPadding = Modifier
|
timestampLayoutModifier = Modifier
|
||||||
contentModifier = Modifier
|
contentModifier = Modifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val threadDecoration = @Composable {
|
||||||
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
|
||||||
if (showThreadDecoration) {
|
if (showThreadDecoration) {
|
||||||
ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp))
|
ThreadDecoration(modifier = Modifier.padding(top = 8.dp, start = 12.dp, end = 12.dp))
|
||||||
}
|
}
|
||||||
if (inReplyToDetails != null) {
|
}
|
||||||
val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
|
val contentWithTimestamp = @Composable {
|
||||||
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
|
WithTimestampLayout(
|
||||||
val text = textForInReplyTo(inReplyToDetails)
|
timestampPosition = timestampPosition,
|
||||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
modifier = timestampLayoutModifier,
|
||||||
ReplyToContent(
|
) {
|
||||||
senderName = senderName,
|
TimelineItemEventContentView(
|
||||||
text = text,
|
content = event.content,
|
||||||
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
isMine = event.isMine,
|
||||||
modifier = Modifier
|
interactionSource = interactionSource,
|
||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
onClick = onMessageClick,
|
||||||
.clip(RoundedCornerShape(6.dp))
|
onLongClick = onMessageLongClick,
|
||||||
.clickable(enabled = true, onClick = inReplyToClick),
|
extraPadding = event.toExtraPadding(),
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = contentModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ContentAndTimestampView(
|
}
|
||||||
timestampPosition = timestampPosition,
|
val inReplyTo = @Composable { inReplyToReady: InReplyTo.Ready ->
|
||||||
modifier = modifierWithPadding,
|
val senderName = inReplyToReady.senderDisplayName ?: inReplyToReady.senderId.value
|
||||||
contentModifier = contentModifier,
|
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToReady)
|
||||||
|
val text = textForInReplyTo(inReplyToReady)
|
||||||
|
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||||
|
ReplyToContent(
|
||||||
|
senderName = senderName,
|
||||||
|
text = text,
|
||||||
|
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.clickable(enabled = true, onClick = inReplyToClick),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
if (inReplyToDetails != null) {
|
||||||
|
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
|
||||||
|
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
||||||
|
threadDecoration()
|
||||||
|
inReplyTo(inReplyToDetails)
|
||||||
|
contentWithTimestamp()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) {
|
||||||
|
threadDecoration()
|
||||||
|
contentWithTimestamp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,6 +696,8 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.First,
|
groupPosition = TimelineItemGroupPosition.First,
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -659,6 +707,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -671,6 +720,8 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
),
|
),
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -680,6 +731,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -710,6 +762,8 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
inReplyTo = aInReplyToReady(replyContent),
|
inReplyTo = aInReplyToReady(replyContent),
|
||||||
groupPosition = TimelineItemGroupPosition.First,
|
groupPosition = TimelineItemGroupPosition.First,
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -719,6 +773,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -733,6 +788,8 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
isThreaded = true,
|
isThreaded = true,
|
||||||
groupPosition = TimelineItemGroupPosition.Last,
|
groupPosition = TimelineItemGroupPosition.Last,
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -742,6 +799,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -784,6 +842,8 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||||
reactionsState = aTimelineItemReactions(count = 0),
|
reactionsState = aTimelineItemReactions(count = 0),
|
||||||
senderDisplayName = if (useDocument) "Document case" else "Text case",
|
senderDisplayName = if (useDocument) "Document case" else "Text case",
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -793,6 +853,7 @@ internal fun TimelineItemEventRowTimestampPreview(
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -816,6 +877,8 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
|
||||||
),
|
),
|
||||||
timelineItemReactions = aTimelineItemReactions(count = 20),
|
timelineItemReactions = aTimelineItemReactions(count = 20),
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -825,6 +888,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -841,6 +905,8 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
senderDisplayName = "a long sender display name to test single line and ellipsis at the end of the line",
|
senderDisplayName = "a long sender display name to test single line and ellipsis at the end of the line",
|
||||||
),
|
),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -850,6 +916,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
@ -862,6 +929,8 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
||||||
internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
||||||
TimelineItemEventRow(
|
TimelineItemEventRow(
|
||||||
event = aTimelineItemEvent(content = aTimelineItemPollContent()),
|
event = aTimelineItemEvent(content = aTimelineItemPollContent()),
|
||||||
|
showReadReceipts = false,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
canReply = true,
|
canReply = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
|
|
@ -871,6 +940,7 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onTimestampClicked = {},
|
onTimestampClicked = {},
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ internal fun TimelineItemReactionsLayoutPreview() = ElementPreview {
|
||||||
},
|
},
|
||||||
addMoreButton = {
|
addMoreButton = {
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_september_add_reaction),
|
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {}
|
onLongClick = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ private fun TimelineItemReactionsView(
|
||||||
onToggleExpandClick: () -> Unit,
|
onToggleExpandClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// In LTR languages we want an incoming message's reactions to be LRT and outgoing to be RTL.
|
// In LTR languages we want an incoming message's reactions to be LTR and outgoing to be RTL.
|
||||||
// For RTL languages it should be the opposite.
|
// For RTL languages it should be the opposite.
|
||||||
val currentLayout = LocalLayoutDirection.current
|
val currentLayout = LocalLayoutDirection.current
|
||||||
val reactionsLayoutDirection = if (!isOutgoing) currentLayout
|
val reactionsLayoutDirection = if (!isOutgoing) currentLayout
|
||||||
|
|
@ -95,7 +95,7 @@ private fun TimelineItemReactionsView(
|
||||||
},
|
},
|
||||||
addMoreButton = {
|
addMoreButton = {
|
||||||
MessagesReactionButton(
|
MessagesReactionButton(
|
||||||
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_september_add_reaction),
|
content = MessagesReactionsButtonContent.Icon(CommonDrawables.ic_add_reaction),
|
||||||
onClick = onMoreReactionsClick,
|
onClick = onMoreReactionsClick,
|
||||||
onLongClick = {}
|
onLongClick = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
||||||
|
|
@ -34,7 +35,7 @@ fun TimelineItemVirtualRow(
|
||||||
) {
|
) {
|
||||||
when (virtual.model) {
|
when (virtual.model) {
|
||||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||||
TimelineItemReadMarkerModel -> return
|
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
||||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier)
|
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SecondaryTabRow
|
import androidx.compose.material3.SecondaryTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -47,7 +48,7 @@ import kotlinx.collections.immutable.ImmutableSet
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiPicker(
|
fun EmojiPicker(
|
||||||
onEmojiSelected: (Emoji) -> Unit,
|
onEmojiSelected: (Emoji) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -28,31 +28,29 @@ import androidx.compose.material.icons.outlined.EmojiSymbols
|
||||||
import androidx.compose.material.icons.outlined.EmojiTransportation
|
import androidx.compose.material.icons.outlined.EmojiTransportation
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import io.element.android.emojibasebindings.EmojibaseCategory
|
import io.element.android.emojibasebindings.EmojibaseCategory
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.features.messages.impl.R
|
||||||
|
|
||||||
@get:StringRes
|
@get:StringRes
|
||||||
val EmojibaseCategory.title: Int get() =
|
val EmojibaseCategory.title: Int
|
||||||
when(this){
|
get() = when (this) {
|
||||||
EmojibaseCategory.People -> CommonStrings.emoji_picker_category_people
|
EmojibaseCategory.People -> R.string.emoji_picker_category_people
|
||||||
EmojibaseCategory.Nature -> CommonStrings.emoji_picker_category_nature
|
EmojibaseCategory.Nature -> R.string.emoji_picker_category_nature
|
||||||
EmojibaseCategory.Foods -> CommonStrings.emoji_picker_category_foods
|
EmojibaseCategory.Foods -> R.string.emoji_picker_category_foods
|
||||||
EmojibaseCategory.Activity -> CommonStrings.emoji_picker_category_activity
|
EmojibaseCategory.Activity -> R.string.emoji_picker_category_activity
|
||||||
EmojibaseCategory.Places -> CommonStrings.emoji_picker_category_places
|
EmojibaseCategory.Places -> R.string.emoji_picker_category_places
|
||||||
EmojibaseCategory.Objects -> CommonStrings.emoji_picker_category_objects
|
EmojibaseCategory.Objects -> R.string.emoji_picker_category_objects
|
||||||
EmojibaseCategory.Symbols -> CommonStrings.emoji_picker_category_symbols
|
EmojibaseCategory.Symbols -> R.string.emoji_picker_category_symbols
|
||||||
EmojibaseCategory.Flags -> CommonStrings.emoji_picker_category_flags
|
EmojibaseCategory.Flags -> R.string.emoji_picker_category_flags
|
||||||
}
|
}
|
||||||
|
|
||||||
val EmojibaseCategory.icon: ImageVector
|
val EmojibaseCategory.icon: ImageVector
|
||||||
get() =
|
get() = when (this) {
|
||||||
when(this){
|
EmojibaseCategory.People -> Icons.Outlined.EmojiPeople
|
||||||
EmojibaseCategory.People -> Icons.Outlined.EmojiPeople
|
EmojibaseCategory.Nature -> Icons.Outlined.EmojiNature
|
||||||
EmojibaseCategory.Nature -> Icons.Outlined.EmojiNature
|
EmojibaseCategory.Foods -> Icons.Outlined.EmojiFoodBeverage
|
||||||
EmojibaseCategory.Foods -> Icons.Outlined.EmojiFoodBeverage
|
EmojibaseCategory.Activity -> Icons.Outlined.EmojiEvents
|
||||||
EmojibaseCategory.Activity -> Icons.Outlined.EmojiEvents
|
EmojibaseCategory.Places -> Icons.Outlined.EmojiTransportation
|
||||||
EmojibaseCategory.Places -> Icons.Outlined.EmojiTransportation
|
EmojibaseCategory.Objects -> Icons.Outlined.EmojiObjects
|
||||||
EmojibaseCategory.Objects -> Icons.Outlined.EmojiObjects
|
EmojibaseCategory.Symbols -> Icons.Outlined.EmojiSymbols
|
||||||
EmojibaseCategory.Symbols -> Icons.Outlined.EmojiSymbols
|
EmojibaseCategory.Flags -> Icons.Outlined.EmojiFlags
|
||||||
EmojibaseCategory.Flags -> Icons.Outlined.EmojiFlags
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ fun TimelineItemEncryptedView(
|
||||||
TimelineItemInformativeView(
|
TimelineItemInformativeView(
|
||||||
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
|
text = stringResource(id = CommonStrings.common_waiting_for_decryption_key),
|
||||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||||
iconResourceId = CommonDrawables.ic_september_decryption_error,
|
iconResourceId = CommonDrawables.ic_waiting_to_decrypt,
|
||||||
extraPadding = extraPadding,
|
extraPadding = extraPadding,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ fun TimelineItemFileView(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
resourceId = CommonDrawables.ic_september_attachment,
|
resourceId = CommonDrawables.ic_attachment,
|
||||||
contentDescription = "OpenFile",
|
contentDescription = "OpenFile",
|
||||||
tint = ElementTheme.materialColors.primary,
|
tint = ElementTheme.materialColors.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -53,6 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemVoiceView(
|
fun TimelineItemVoiceView(
|
||||||
|
|
@ -90,7 +96,7 @@ fun TimelineItemVoiceView(
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
WaveformPlaybackView(
|
WaveformPlaybackView(
|
||||||
showCursor = state.button == VoiceMessageState.Button.Pause,
|
showCursor = state.showCursor,
|
||||||
playbackProgress = state.progress,
|
playbackProgress = state.progress,
|
||||||
waveform = content.waveform,
|
waveform = content.waveform,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -147,19 +153,40 @@ private fun RetryButton(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress button is shown when the voice message is being downloaded.
|
||||||
|
*
|
||||||
|
* The progress indicator is optimistic and displays a pause button (which
|
||||||
|
* indicates the audio is playing) for 2 seconds before revealing the
|
||||||
|
* actual progress indicator.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProgressButton() {
|
private fun ProgressButton(
|
||||||
|
displayImmediately: Boolean = false,
|
||||||
|
) {
|
||||||
|
var canDisplay by remember { mutableStateOf(displayImmediately) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(2000L)
|
||||||
|
canDisplay = true
|
||||||
|
}
|
||||||
CustomIconButton(
|
CustomIconButton(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
enabled = false,
|
enabled = false,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
if (canDisplay) {
|
||||||
modifier = Modifier
|
CircularProgressIndicator(
|
||||||
.padding(2.dp)
|
modifier = Modifier
|
||||||
.size(16.dp),
|
.padding(2.dp)
|
||||||
color = ElementTheme.colors.iconSecondary,
|
.size(16.dp),
|
||||||
strokeWidth = 2.dp,
|
color = ElementTheme.colors.iconSecondary,
|
||||||
)
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
resourceId = R.drawable.pause,
|
||||||
|
contentDescription = stringResource(id = CommonStrings.a11y_pause),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,3 +255,12 @@ internal fun TimelineItemVoiceViewUnifiedPreview() = ElementPreview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ProgressButtonPreview() = ElementPreview {
|
||||||
|
Row {
|
||||||
|
ProgressButton(displayImmediately = true)
|
||||||
|
ProgressButton(displayImmediately = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class ReadReceiptViewState(
|
||||||
|
val sendState: LocalEventSendState?,
|
||||||
|
val isLastOutgoingMessage: Boolean,
|
||||||
|
val receipts: ImmutableList<ReadReceiptData>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class ReadReceiptViewStateProvider : PreviewParameterProvider<ReadReceiptViewState> {
|
||||||
|
override val values: Sequence<ReadReceiptViewState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aReadReceiptViewState(),
|
||||||
|
aReadReceiptViewState(sendState = LocalEventSendState.NotSentYet),
|
||||||
|
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(1) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(2) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(3) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(4) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
aReadReceiptViewState(
|
||||||
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
|
receipts = mutableListOf<ReadReceiptData>().apply { repeat(5) { add(aReadReceiptData(it)) } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aReadReceiptViewState(
|
||||||
|
sendState: LocalEventSendState? = null,
|
||||||
|
isLastOutgoingMessage: Boolean = true,
|
||||||
|
receipts: List<ReadReceiptData> = emptyList(),
|
||||||
|
) = ReadReceiptViewState(
|
||||||
|
sendState = sendState,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
receipts = receipts.toImmutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aReadReceiptData(
|
||||||
|
index: Int,
|
||||||
|
avatarData: AvatarData = anAvatarData(
|
||||||
|
id = "$index",
|
||||||
|
size = AvatarSize.TimelineReadReceipt
|
||||||
|
),
|
||||||
|
formattedDate: String = "12:34",
|
||||||
|
) = ReadReceiptData(
|
||||||
|
avatarData = avatarData,
|
||||||
|
formattedDate = formattedDate,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.element.android.appconfig.TimelineConfig
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.getBestName
|
||||||
|
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
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemReadReceiptView(
|
||||||
|
state: ReadReceiptViewState,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
onReadReceiptsClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (state.receipts.isNotEmpty()) {
|
||||||
|
if (showReadReceipts) {
|
||||||
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
|
ReadReceiptsAvatars(
|
||||||
|
receipts = state.receipts,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.clickable { onReadReceiptsClicked() }
|
||||||
|
.padding(2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else when (state.sendState) {
|
||||||
|
LocalEventSendState.NotSentYet -> {
|
||||||
|
ReadReceiptsRow(modifier) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
resourceId = CommonDrawables.ic_sending,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocalEventSendState.Canceled -> Unit
|
||||||
|
is LocalEventSendState.SendingFailed -> {
|
||||||
|
// Error? The timestamp is already displayed in red
|
||||||
|
}
|
||||||
|
null,
|
||||||
|
is LocalEventSendState.Sent -> {
|
||||||
|
if (state.isLastOutgoingMessage) {
|
||||||
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
resourceId = CommonDrawables.ic_sent,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReadReceiptsRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(AvatarSize.TimelineReadReceipt.dp + 8.dp)
|
||||||
|
.padding(horizontal = 18.dp),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReadReceiptsAvatars(
|
||||||
|
receipts: ImmutableList<ReadReceiptData>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val avatarSize = AvatarSize.TimelineReadReceipt.dp
|
||||||
|
val avatarStrokeSize = 1.dp
|
||||||
|
val avatarStrokeColor = MaterialTheme.colorScheme.background
|
||||||
|
val receiptDescription = computeReceiptDescription(receipts)
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
stateDescription = receiptDescription
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
) {
|
||||||
|
receipts
|
||||||
|
.take(TimelineConfig.maxReadReceiptToDisplay)
|
||||||
|
.reversed()
|
||||||
|
.forEachIndexed { index, readReceiptData ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = (12.dp + avatarStrokeSize * 2) * index)
|
||||||
|
.size(size = avatarSize + avatarStrokeSize * 2)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(avatarStrokeColor)
|
||||||
|
.zIndex(index.toFloat()),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
avatarData = readReceiptData.avatarData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (receipts.size > TimelineConfig.maxReadReceiptToDisplay) {
|
||||||
|
Text(
|
||||||
|
text = "+" + (receipts.size - TimelineConfig.maxReadReceiptToDisplay),
|
||||||
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun computeReceiptDescription(receipts: ImmutableList<ReadReceiptData>): String {
|
||||||
|
return when (receipts.size) {
|
||||||
|
0 -> "" // Cannot happen
|
||||||
|
1 -> stringResource(
|
||||||
|
id = CommonStrings.a11y_read_receipts_single,
|
||||||
|
receipts[0].avatarData.getBestName()
|
||||||
|
)
|
||||||
|
2 -> stringResource(
|
||||||
|
id = CommonStrings.a11y_read_receipts_multiple,
|
||||||
|
receipts[0].avatarData.getBestName(),
|
||||||
|
receipts[1].avatarData.getBestName(),
|
||||||
|
)
|
||||||
|
else -> pluralStringResource(
|
||||||
|
id = CommonPlurals.a11y_read_receipts_multiple_with_others,
|
||||||
|
count = receipts.size - 1,
|
||||||
|
receipts[0].avatarData.getBestName(),
|
||||||
|
receipts.size - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemReactionsViewPreview(
|
||||||
|
@PreviewParameter(ReadReceiptViewStateProvider::class) state: ReadReceiptViewState,
|
||||||
|
) = ElementPreview {
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = state,
|
||||||
|
showReadReceipts = true,
|
||||||
|
onReadReceiptsClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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 io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun ReadReceiptBottomSheet(
|
||||||
|
state: ReadReceiptBottomSheetState,
|
||||||
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val isVisible = state.selectedEvent != null
|
||||||
|
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
if (isVisible) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier,
|
||||||
|
// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
|
||||||
|
// .imePadding()
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.hide()
|
||||||
|
state.eventSink(ReadReceiptBottomSheetEvents.Dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ReadReceiptBottomSheetContent(
|
||||||
|
state = state,
|
||||||
|
onUserDataClicked = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
sheetState.hide()
|
||||||
|
state.eventSink(ReadReceiptBottomSheetEvents.Dismiss)
|
||||||
|
onUserDataClicked.invoke(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// FIXME remove after https://issuetracker.google.com/issues/275849044
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.ReadReceiptBottomSheetContent(
|
||||||
|
state: ReadReceiptBottomSheetState,
|
||||||
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(id = CommonStrings.common_seen_by))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val receipts = state.selectedEvent?.readReceiptState?.receipts.orEmpty()
|
||||||
|
receipts.forEach {
|
||||||
|
val userId = UserId(it.avatarData.id)
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = Modifier.clickable { onUserDataClicked(userId) },
|
||||||
|
matrixUser = MatrixUser(
|
||||||
|
userId = userId,
|
||||||
|
displayName = it.avatarData.name,
|
||||||
|
avatarUrl = it.avatarData.url,
|
||||||
|
),
|
||||||
|
avatarSize = AvatarSize.ReadReceiptList,
|
||||||
|
trailingContent = {
|
||||||
|
Text(
|
||||||
|
text = it.formattedDate,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) = ElementPreview {
|
||||||
|
// TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed
|
||||||
|
Column {
|
||||||
|
ReadReceiptBottomSheetContent(
|
||||||
|
state = state,
|
||||||
|
onUserDataClicked = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
|
sealed interface ReadReceiptBottomSheetEvents {
|
||||||
|
data class EventSelected(val event: TimelineItem.Event) : ReadReceiptBottomSheetEvents
|
||||||
|
data object Dismiss : ReadReceiptBottomSheetEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ReadReceiptBottomSheetPresenter @Inject constructor(
|
||||||
|
) : Presenter<ReadReceiptBottomSheetState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ReadReceiptBottomSheetState {
|
||||||
|
var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
fun handleEvent(event: ReadReceiptBottomSheetEvents) {
|
||||||
|
@Suppress("LiftReturnOrAssignment")
|
||||||
|
when (event) {
|
||||||
|
is ReadReceiptBottomSheetEvents.EventSelected -> {
|
||||||
|
selectedEvent = event.event
|
||||||
|
}
|
||||||
|
ReadReceiptBottomSheetEvents.Dismiss -> {
|
||||||
|
selectedEvent = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadReceiptBottomSheetState(
|
||||||
|
selectedEvent = selectedEvent,
|
||||||
|
eventSink = { handleEvent(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ReadReceiptBottomSheetState(
|
||||||
|
val selectedEvent: TimelineItem.Event?,
|
||||||
|
val eventSink: (ReadReceiptBottomSheetEvents) -> Unit,
|
||||||
|
)
|
||||||
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