Merge branch 'develop' into feature/fga/room-version-upgrade

This commit is contained in:
ganfra 2025-06-16 20:46:28 +02:00
commit e2b1ab2632
150 changed files with 2027 additions and 1121 deletions

View file

@ -44,6 +44,7 @@ import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -52,6 +53,7 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
class CallScreenPresenter @AssistedInject constructor(
@Assisted private val callType: CallType,
@ -165,6 +167,13 @@ class CallScreenPresenter @AssistedInject constructor(
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
sendHangupMessage(widgetId, interceptor)
isJoinedCall = false
coroutineScope.launch {
// Wait for a couple of seconds to receive the hangup message
// If we don't get it in time, we close the screen anyway
delay(2.seconds)
close(callWidgetDriver.value, navigator)
}
} else {
coroutineScope.launch {
close(callWidgetDriver.value, navigator)

View file

@ -38,6 +38,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason
import io.element.android.features.call.impl.utils.WebViewAudioManager
import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
@ -105,6 +106,14 @@ internal fun CallScreenView(
} else {
var webViewAudioManager by remember { mutableStateOf<WebViewAudioManager?>(null) }
val coroutineScope = rememberCoroutineScope()
var invalidAudioDeviceReason by remember { mutableStateOf<InvalidAudioDeviceReason?>(null) }
invalidAudioDeviceReason?.let {
InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) {
invalidAudioDeviceReason = null
}
}
CallWebView(
modifier = Modifier
.padding(padding)
@ -130,7 +139,11 @@ internal fun CallScreenView(
},
onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) },
)
webViewAudioManager = WebViewAudioManager(webView, coroutineScope)
webViewAudioManager = WebViewAudioManager(
webView = webView,
coroutineScope = coroutineScope,
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
@ -157,6 +170,21 @@ internal fun CallScreenView(
}
}
@Composable
private fun InvalidAudioDeviceDialog(
invalidAudioDeviceReason: InvalidAudioDeviceReason,
onDismiss: () -> Unit,
) {
ErrorDialog(
content = when (invalidAudioDeviceReason) {
InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED -> {
stringResource(R.string.call_invalid_audio_device_bluetooth_devices_disabled)
}
},
onSubmit = onDismiss,
)
}
@Composable
private fun CallWebView(
url: AsyncData<String>,
@ -277,3 +305,9 @@ internal fun CallScreenPipViewPreview(
requestPermissions = { _, _ -> },
)
}
@PreviewsDayNight
@Composable
internal fun InvalidAudioDeviceDialogPreview() = ElementPreview {
InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {}
}

View file

@ -40,8 +40,22 @@ import kotlin.time.Duration.Companion.milliseconds
class WebViewAudioManager(
private val webView: WebView,
private val coroutineScope: CoroutineScope,
private val onInvalidAudioDeviceAdded: (InvalidAudioDeviceReason) -> Unit,
) {
// The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
/**
* Whether to disable bluetooth audio devices. This must be done on Android versions lower than Android 12,
* since the WebView approach breaks when using the legacy Bluetooth audio APIs.
*/
private val disableBluetoothAudioDevices = Build.VERSION.SDK_INT < Build.VERSION_CODES.S
/**
* This flag indicates whether the WebView audio is enabled or not. By default, it is enabled.
*/
private val isWebViewAudioEnabled = AtomicBoolean(true)
/**
* The list of device types that are considered as communication devices, sorted by likelihood of it being used for communication.
*/
private val wantedDeviceTypes = listOf(
// Paired bluetooth device with microphone
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
@ -60,6 +74,10 @@ class WebViewAudioManager(
private val audioManager = webView.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
/**
* This wake lock is used to turn off the screen when the proximity sensor is triggered during a call,
* if the selected audio device is the built-in earpiece.
*/
private val proximitySensorWakeLock by lazy {
webView.context.getSystemService<PowerManager>()
?.takeIf { it.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK) }
@ -296,12 +314,13 @@ class WebViewAudioManager(
* @param availableDevices The list of available audio devices to select from. If not provided, it will use the current list of audio devices.
*/
private fun selectDefaultAudioDevice(availableDevices: List<AudioDeviceInfo> = listAudioDevices()) {
val selectedDevice = availableDevices.minByOrNull {
wantedDeviceTypes.indexOf(it.type).let { index ->
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
if (index == -1) Int.MAX_VALUE else index
val selectedDevice = availableDevices
.minByOrNull {
wantedDeviceTypes.indexOf(it.type).let { index ->
// If the device type is not in the wantedDeviceTypes list, we give it a low priority
if (index == -1) Int.MAX_VALUE else index
}
}
}
expectedNewCommunicationDeviceId = selectedDevice?.id
audioManager.selectAudioDevice(selectedDevice)
@ -361,6 +380,13 @@ class WebViewAudioManager(
// On Android 11 and lower, we don't have the concept of communication devices
// We have to call the right methods based on the device type
if (device != null) {
if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO && disableBluetoothAudioDevices) {
Timber.w("Bluetooth audio devices are disabled on this Android version")
setAudioEnabled(false)
onInvalidAudioDeviceAdded(InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED)
return
}
setAudioEnabled(true)
isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
} else {
@ -380,6 +406,19 @@ class WebViewAudioManager(
proximitySensorWakeLock?.release()
}
}
/**
* Sets whether the audio is enabled for Element Call in the WebView.
* It will only perform the change if the audio state has changed.
*/
private fun setAudioEnabled(enabled: Boolean) {
coroutineScope.launch(Dispatchers.Main) {
Timber.d("Setting audio enabled in Element Call: $enabled")
if (isWebViewAudioEnabled.getAndSet(enabled) != enabled) {
webView.evaluateJavascript("controls.setAudioEnabled($enabled);", null)
}
}
}
}
/**
@ -434,6 +473,10 @@ private fun isBuiltIn(type: Int): Boolean = when (type) {
else -> false
}
enum class InvalidAudioDeviceReason {
BT_AUDIO_DEVICE_DISABLED,
}
/**
* This class is used to serialize the audio device information to JSON.
*/

View file

@ -11,6 +11,10 @@ import android.webkit.WebView
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Documentation about the `controls` command can be found here:
* https://github.com/element-hq/element-call/blob/livekit/docs/controls.md#picture-in-picture
*/
class WebViewPipController(
private val webView: WebView,
) : PipController {

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Appel en cours"</string>
<string name="call_foreground_service_message_android">"Cliquez pour retourner à lappel."</string>
<string name="call_foreground_service_title_android">"☎️ Appel en cours"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call ne prend pas en charge lutilisation daccessoires Bluetooth dans cette version dAndroid. Sélectionnez une autre sortie audio."</string>
<string name="screen_incoming_call_subtitle_android">"Appel Element entrant"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Pågående samtale"</string>
<string name="call_foreground_service_message_android">"Trykk for å gå tilbake til samtalen"</string>
<string name="call_foreground_service_title_android">"☎️ Samtale pågår"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call støtter ikke bruk av Bluetooth-lydenheter i denne Android-versjonen. Velg en annen lydenhet."</string>
<string name="screen_incoming_call_subtitle_android">"Innkommende Element-anrop"</string>
</resources>

View file

@ -3,5 +3,6 @@
<string name="call_foreground_service_channel_title_android">"Ongoing call"</string>
<string name="call_foreground_service_message_android">"Tap to return to the call"</string>
<string name="call_foreground_service_title_android">"☎️ Call in progress"</string>
<string name="call_invalid_audio_device_bluetooth_devices_disabled">"Element Call does not support using Bluetooth audio devices in this Android version. Please select a different audio device."</string>
<string name="screen_incoming_call_subtitle_android">"Incoming Element Call"</string>
</resources>

View file

@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalComposeUiApi::class)
package io.element.android.features.logout.impl
import androidx.compose.foundation.clickable
@ -32,7 +30,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.graphics.Color

View file

@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.focus.FocusDirection
@ -170,7 +169,6 @@ fun LoginPasswordView(
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun LoginForm(
state: LoginPasswordState,

View file

@ -89,5 +89,6 @@ Prøv å logge på manuelt, eller skann QR-koden med en annen enhet."</string>
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon."</string>
<string name="screen_server_confirmation_message_register">"Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine."</string>
<string name="screen_server_confirmation_title_login">"Du er i ferd med å logge inn på %1$s"</string>
<string name="screen_server_confirmation_title_picker_mode">"Velg kontoleverandør"</string>
<string name="screen_server_confirmation_title_register">"Du er i ferd med å opprette en konto på %1$s"</string>
</resources>

View file

@ -76,8 +76,6 @@ import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@ -225,7 +223,6 @@ private fun MessageShieldDialog(state: TimelineState) {
)
}
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
private fun TimelinePrefetchingHelper(
lazyListState: LazyListState,

View file

@ -21,7 +21,6 @@ internal fun ATimelineItemEventRow(
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
renderReadReceipts: Boolean = false,
isLastOutgoingMessage: Boolean = false,
isHighlighted: Boolean = false,
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
) = TimelineItemEventRow(
event = event,
@ -29,7 +28,6 @@ internal fun ATimelineItemEventRow(
renderReadReceipts = renderReadReceipts,
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = isHighlighted,
onEventClick = {},
onLongClick = {},
onLinkClick = {},

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
@ -59,7 +58,6 @@ private val avatarRadius = AvatarSize.TimelineSender.dp / 2
private const val BUBBLE_WIDTH_RATIO = 0.78f
private val MIN_BUBBLE_WIDTH = 80.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessageEventBubble(
state: BubbleState,
@ -184,7 +182,7 @@ internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStateProvider::cl
contentAlignment = Alignment.Center,
) {
Text(
text = "${state.groupPosition.javaClass.simpleName} m:${state.isMine.to01()} h:${state.isHighlighted.to01()}",
text = "${state.groupPosition.javaClass.simpleName} isMine:${state.isMine.to01()}",
style = ElementTheme.typography.fontBodyXsRegular,
)
}

View file

@ -7,12 +7,8 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ripple
@ -28,17 +24,14 @@ import io.element.android.libraries.designsystem.theme.components.Surface
private val CORNER_RADIUS = 8.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessageStateEventContainer(
@Suppress("UNUSED_PARAMETER") isHighlighted: Boolean,
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {},
) {
// Ignore isHighlighted for now, we need a design decision on it.
val backgroundColor = Color.Transparent
val shape = RoundedCornerShape(CORNER_RADIUS)
Surface(
@ -60,22 +53,9 @@ fun MessageStateEventContainer(
@PreviewsDayNight
@Composable
internal fun MessageStateEventContainerPreview() = ElementPreview {
Column {
MessageStateEventContainer(
isHighlighted = false,
interactionSource = remember { MutableInteractionSource() },
onClick = {},
onLongClick = {},
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
MessageStateEventContainer(
isHighlighted = true,
interactionSource = remember { MutableInteractionSource() },
onClick = {},
onLongClick = {},
) {
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
}
}
MessageStateEventContainer(
interactionSource = remember { MutableInteractionSource() },
onClick = {},
onLongClick = {},
)
}

View file

@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.timeline.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
@ -50,7 +49,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
@Composable
@OptIn(ExperimentalFoundationApi::class)
@Suppress("ModifierClickableOrder") // This is needed to display the right ripple shape
fun MessagesReactionButton(
onClick: () -> Unit,

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@ -39,7 +38,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TimelineItemCallNotifyView(
event: TimelineItem.Event,

View file

@ -32,7 +32,6 @@ import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalViewConfiguration
@ -120,7 +119,6 @@ fun TimelineItemEventRow(
timelineProtectionState: TimelineProtectionState,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
isHighlighted: Boolean,
onEventClick: () -> Unit,
onLongClick: () -> Unit,
onLinkClick: (Link) -> Unit,
@ -196,7 +194,6 @@ fun TimelineItemEventRow(
TimelineItemEventRowContent(
event = event,
timelineProtectionState = timelineProtectionState,
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onContentClick = onContentClick,
@ -230,7 +227,6 @@ fun TimelineItemEventRow(
TimelineItemEventRowContent(
event = event,
timelineProtectionState = timelineProtectionState,
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
interactionSource = interactionSource,
onContentClick = onContentClick,
@ -281,12 +277,10 @@ private fun SwipeSensitivity(
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TimelineItemEventRowContent(
event: TimelineItem.Event,
timelineProtectionState: TimelineProtectionState,
isHighlighted: Boolean,
timelineRoomInfo: TimelineRoomInfo,
interactionSource: MutableInteractionSource,
onContentClick: () -> Unit,
@ -340,7 +334,6 @@ private fun TimelineItemEventRowContent(
val bubbleState = BubbleState(
groupPosition = event.groupPosition,
isMine = event.isMine,
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
)
MessageEventBubble(

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
@ -21,6 +20,7 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
@ -48,7 +49,6 @@ import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlin.time.DurationUnit
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TimelineItemRow(
timelineItem: TimelineItem,
@ -113,7 +113,6 @@ internal fun TimelineItemRow(
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onContentClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
@ -140,6 +139,8 @@ internal fun TimelineItemRow(
} else {
timelineItem.safeSenderName
}
// For Polls, allow the answers to be traversed by Talkback
isTraversalGroup = timelineItem.content is TimelineItemPollContent
}
// Custom clickable that applies over the whole item for accessibility
.then(
@ -157,7 +158,6 @@ internal fun TimelineItemRow(
renderReadReceipts = renderReadReceipts,
timelineProtectionState = timelineProtectionState,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onEventClick = { onContentClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick,

View file

@ -40,7 +40,6 @@ fun TimelineItemStateEventRow(
event: TimelineItem.Event,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onReadReceiptsClick: (event: TimelineItem.Event) -> Unit,
@ -60,7 +59,6 @@ fun TimelineItemStateEventRow(
contentAlignment = Alignment.Center
) {
MessageStateEventContainer(
isHighlighted = isHighlighted,
interactionSource = interactionSource,
onClick = onClick,
onLongClick = onLongClick,
@ -107,7 +105,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
),
renderReadReceipts = true,
isLastOutgoingMessage = false,
isHighlighted = false,
onClick = {},
onLongClick = {},
onReadReceiptsClick = {},

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
@ -57,7 +56,6 @@ import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemImageView(
content: TimelineItemImageContent,

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
@ -39,7 +38,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
private const val STICKER_SIZE_IN_DP = 128
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemStickerView(
content: TimelineItemStickerContent,

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannedString
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@ -67,7 +66,6 @@ import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimelineItemVideoView(
content: TimelineItemVideoContent,

View file

@ -13,7 +13,6 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
data class BubbleState(
val groupPosition: TimelineItemGroupPosition,
val isMine: Boolean,
val isHighlighted: Boolean,
val timelineRoomInfo: TimelineRoomInfo,
) {
/** True to cut out the top start corner of the bubble, to give margin for the sender avatar. */

View file

@ -21,15 +21,11 @@ open class BubbleStateProvider : PreviewParameterProvider<BubbleState> {
TimelineItemGroupPosition.None,
).map { groupPosition ->
sequenceOf(false, true).map { isMine ->
sequenceOf(false, true).map { isHighlighted ->
aBubbleState(
groupPosition = groupPosition,
isMine = isMine,
isHighlighted = isHighlighted,
)
}
aBubbleState(
groupPosition = groupPosition,
isMine = isMine,
)
}
.flatten()
}
.flatten()
}
@ -37,11 +33,9 @@ open class BubbleStateProvider : PreviewParameterProvider<BubbleState> {
internal fun aBubbleState(
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First,
isMine: Boolean = false,
isHighlighted: Boolean = false,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
) = BubbleState(
groupPosition = groupPosition,
isMine = isMine,
isHighlighted = isHighlighted,
timelineRoomInfo = timelineRoomInfo,
)

View file

@ -35,6 +35,15 @@
<string name="screen_room_timeline_less_reactions">"Afficher moins"</string>
<string name="screen_room_timeline_message_copied">"Message copié"</string>
<string name="screen_room_timeline_no_permission_to_post">"Vous nêtes pas autorisé à publier dans ce salon"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"%1$d membre a réagi avec %2$s"</item>
<item quantity="other">"%1$d membres ont réagi avec %2$s"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="one">"Vous et %1$d membre avez réagi avec %2$s"</item>
<item quantity="other">"Vous et %1$d membres avez réagi avec %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"Vous avez réagi avec %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Afficher moins"</string>
<string name="screen_room_timeline_reactions_show_more">"Afficher plus"</string>
<string name="screen_room_timeline_read_marker_title">"Nouveau"</string>

View file

@ -35,6 +35,15 @@
<string name="screen_room_timeline_less_reactions">"Vis mindre"</string>
<string name="screen_room_timeline_message_copied">"Melding kopiert"</string>
<string name="screen_room_timeline_no_permission_to_post">"Du har ikke tillatelse til å legge ut innlegg i dette rommet"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"%1$d medlem reagerte med %2$s"</item>
<item quantity="other">"%1$d medlemmer reagerte med %2$s"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="one">"Du og %1$d medlem reagerte med%2$s"</item>
<item quantity="other">"Du og %1$d medlemmer reagerte med%2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"Du reagerte med %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Vis mindre"</string>
<string name="screen_room_timeline_reactions_show_more">"Vis mer"</string>
<string name="screen_room_timeline_read_marker_title">"Ny"</string>

View file

@ -28,13 +28,22 @@
<string name="screen_room_mentions_at_room_title">"Everyone"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
<string name="screen_room_retry_send_menu_title">"Your message failed to send"</string>
<string name="screen_room_timeline_add_reaction">"Add emoji"</string>
<string name="screen_room_timeline_add_reaction">"Add a reaction"</string>
<string name="screen_room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="screen_room_timeline_legacy_call">"Unsupported call. Ask if the caller can use the new Element X app."</string>
<string name="screen_room_timeline_less_reactions">"Show less"</string>
<string name="screen_room_timeline_message_copied">"Message copied"</string>
<string name="screen_room_timeline_no_permission_to_post">"You do not have permission to post to this room"</string>
<plurals name="screen_room_timeline_reaction_a11y">
<item quantity="one">"%1$d member reacted with %2$s"</item>
<item quantity="other">"%1$d members reacted with %2$s"</item>
</plurals>
<plurals name="screen_room_timeline_reaction_including_you_a11y">
<item quantity="one">"You and %1$d member reacted with %2$s"</item>
<item quantity="other">"You and %1$d members reacted with %2$s"</item>
</plurals>
<string name="screen_room_timeline_reaction_you_a11y">"You reacted with %1$s"</string>
<string name="screen_room_timeline_reactions_show_less">"Show less"</string>
<string name="screen_room_timeline_reactions_show_more">"Show more"</string>
<string name="screen_room_timeline_read_marker_title">"New"</string>

View file

@ -47,7 +47,10 @@ class TimelineItemPollViewTest {
)
}
val answer = content.answerItems[answerIndex].answer
rule.onNode(hasText(answer.text)).performClick()
rule.onNode(
matcher = hasText(answer.text),
useUnmergedTree = true,
).performClick()
eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id))
}

View file

@ -20,9 +20,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.poll.api.R
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
@ -32,14 +36,42 @@ import io.element.android.libraries.designsystem.theme.progressIndicatorTrackCol
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PollAnswerView(
answerItem: PollAnswerItem,
modifier: Modifier = Modifier,
) {
val nbVotesText = pluralStringResource(
id = CommonPlurals.common_poll_votes_count,
count = answerItem.votesCount,
answerItem.votesCount,
)
val a11yText = buildString {
val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter)
append(answerItem.answer.text.removeSuffix("."))
if (answerItem.showVotes) {
append(sentenceDelimiter)
append(nbVotesText)
if (answerItem.votesCount != 0) {
append(sentenceDelimiter)
(answerItem.percentage * 100).toInt().let { percent ->
append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent))
}
}
if (answerItem.isWinner) {
append(sentenceDelimiter)
append(stringResource(R.string.a11y_polls_winning_answer))
}
}
}
Row(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.clearAndSetSemantics {
contentDescription = a11yText
},
) {
Icon(
imageVector = if (answerItem.isSelected) {
@ -70,11 +102,6 @@ internal fun PollAnswerView(
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
)
if (answerItem.showVotes) {
val text = pluralStringResource(
id = CommonPlurals.common_poll_votes_count,
count = answerItem.votesCount,
answerItem.votesCount
)
Row(
modifier = Modifier.align(Alignment.Bottom),
verticalAlignment = Alignment.CenterVertically,
@ -87,13 +114,13 @@ internal fun PollAnswerView(
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = text,
text = nbVotesText,
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textPrimary,
)
} else {
Text(
text = text,
text = nbVotesText,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d pour cent du total des votes"</item>
<item quantity="other">"%1$d pour cent du total des votes"</item>
</plurals>
<string name="a11y_polls_winning_answer">"Cest la réponse gagnante"</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d prosent av totalt antall stemmer"</item>
<item quantity="other">"%1$d prosent av totalt antall stemmer"</item>
</plurals>
<string name="a11y_polls_winning_answer">"Dette er vinnersvaret"</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d percent of total votes"</item>
<item quantity="other">"%1$d percents of total votes"</item>
</plurals>
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
</resources>

View file

@ -143,7 +143,7 @@ fun CreatePollView(
trailingContent = ListItemContent.Custom {
Icon(
imageVector = CompoundIcons.Delete(),
contentDescription = null,
contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text),
modifier = Modifier.clickable(answer.canDelete) {
state.eventSink(CreatePollEvents.RemoveAnswer(index))
},

View file

@ -8,7 +8,6 @@
package io.element.android.features.poll.impl.history
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -30,6 +29,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -52,7 +53,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PollHistoryView(
state: PollHistoryState,
@ -179,7 +180,9 @@ private fun PollHistoryList(
if (pollHistoryItems.isEmpty()) {
item {
Column(
modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp),
modifier = Modifier
.fillParentMaxSize()
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -192,7 +195,9 @@ private fun PollHistoryList(
text = emptyStringResource,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
textAlign = TextAlign.Center,
)
@ -228,7 +233,10 @@ private fun PollHistoryItemRow(
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
modifier = modifier.semantics(mergeDescendants = true) {
// Allow the answers to be traversed by Talkback
isTraversalGroup = true
},
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
shape = RoundedCornerShape(size = 12.dp)
) {

View file

@ -5,6 +5,7 @@
<string name="screen_create_poll_anonymous_headline">"Masquer les votes"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Vos modifications nont pas été enregistrées. Êtes-vous certain de vouloir quitter ?"</string>
<string name="screen_create_poll_delete_option_a11y">"Supprimer loption %1$s"</string>
<string name="screen_create_poll_question_desc">"Question ou sujet"</string>
<string name="screen_create_poll_question_hint">"Quel est le sujet du sondage ?"</string>
<string name="screen_create_poll_title">"Créer un sondage"</string>

View file

@ -5,6 +5,7 @@
<string name="screen_create_poll_anonymous_headline">"Skjul stemmer"</string>
<string name="screen_create_poll_answer_hint">"Alternativ %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Endringene dine er ikke lagret. Er du sikker på at du vil gå tilbake?"</string>
<string name="screen_create_poll_delete_option_a11y">"Slett alternativet %1$s"</string>
<string name="screen_create_poll_question_desc">"Spørsmål eller emne"</string>
<string name="screen_create_poll_question_hint">"Hva handler avstemningen om?"</string>
<string name="screen_create_poll_title">"Opprett avstemning"</string>

View file

@ -5,6 +5,7 @@
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
<string name="screen_create_poll_delete_option_a11y">"Delete option %1$s"</string>
<string name="screen_create_poll_question_desc">"Question or topic"</string>
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
<string name="screen_create_poll_title">"Create Poll"</string>

View file

@ -131,7 +131,10 @@ class PollHistoryViewTest {
rule.setPollHistoryViewView(
state = state,
)
rule.onNodeWithText(answer.text).performClick()
rule.onNodeWithText(
text = answer.text,
useUnmergedTree = true,
).performClick()
eventsRecorder.assertSingle(
PollHistoryEvents.SelectPollAnswer(eventId, answer.id)
)

View file

@ -38,6 +38,7 @@ 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.Text
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
@ -149,7 +150,7 @@ private fun NotificationSettingsContentView(
Text(stringResource(R.string.full_screen_intent_banner_message))
},
onClick = {
state.fullScreenIntentPermissionsState.openFullScreenIntentSettings()
state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvents.OpenSettings)
}
)
}

View file

@ -12,7 +12,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -136,7 +135,6 @@ fun RoomMemberListView(
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
private fun RoomMemberList(
roomMembers: AsyncData<RoomMembers>,

View file

@ -33,7 +33,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -55,7 +54,6 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private val userProfilePresenter = userProfilePresenterFactory.create(roomMemberId)
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope()

View file

@ -11,7 +11,6 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -225,7 +224,6 @@ fun ChangeRolesView(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SearchResultsList(
currentRole: RoomMember.Role,

View file

@ -12,6 +12,8 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentSet
@ -31,10 +33,12 @@ internal fun aRoomsContentState(
securityBannerState: SecurityBannerState = SecurityBannerState.None,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(),
batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(),
seenRoomInvites: Set<RoomId> = emptySet(),
) = RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsState,
batteryOptimizationState = batteryOptimizationState,
summaries = summaries,
seenRoomInvites = seenRoomInvites.toPersistentSet(),
)

View file

@ -52,6 +52,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val batteryOptimizationPresenter: Presenter<BatteryOptimizationState>,
private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val appPreferencesStore: AppPreferencesStore,
@ -248,6 +250,7 @@ class RoomListPresenter @Inject constructor(
RoomListContentState.Rooms(
securityBannerState = securityBannerState,
fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(),
batteryOptimizationState = batteryOptimizationPresenter.present(),
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList(),
seenRoomInvites = seenRoomInvites.toPersistentSet(),
)

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
@ -78,6 +79,7 @@ sealed interface RoomListContentState {
data class Rooms(
val securityBannerState: SecurityBannerState,
val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState,
val batteryOptimizationState: BatteryOptimizationState,
val summaries: ImmutableList<RoomListRoomSummary>,
val seenRoomInvites: ImmutableSet<RoomId>,
) : RoomListContentState

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -45,6 +46,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(contentState = aSkeletonContentState()),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
)
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.features.roomlist.impl.R
import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
@Composable
internal fun BatteryOptimizationBanner(
state: BatteryOptimizationState,
modifier: Modifier = Modifier,
) {
Announcement(
modifier = modifier.roomListBannerPadding(),
title = stringResource(R.string.banner_battery_optimization_title_android),
description = stringResource(R.string.banner_battery_optimization_content_android),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
),
)
}
@PreviewsDayNight
@Composable
internal fun BatteryOptimizationBannerPreview() = ElementPreview {
BatteryOptimizationBanner(
state = aBatteryOptimizationState(),
)
}

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@ -29,8 +30,8 @@ fun FullScreenIntentPermissionBanner(
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = state.dismissFullScreenIntentBanner,
onActionClick = state.openFullScreenIntentSettings,
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) },
),
modifier = modifier.roomListBannerPadding(),
)

View file

@ -149,7 +149,7 @@ private fun EmptyView(
onDismissClick = { eventSink(RoomListEvents.DismissBanner) },
)
}
else -> Unit
SecurityBannerState.None -> Unit
}
}
}
@ -234,6 +234,10 @@ private fun RoomsViewList(
item {
FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState)
}
} else if (state.batteryOptimizationState.shouldDisplayBanner) {
item {
BatteryOptimizationBanner(state = state.batteryOptimizationState)
}
}
}

View file

@ -7,7 +7,6 @@
package io.element.android.features.roomlist.impl.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@ -161,7 +160,6 @@ internal fun RoomSummaryRow(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RoomSummaryScaffoldRow(
room: RoomListRoomSummary,

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_battery_optimization_content_android">"Disable battery optimization for this app, to make sure all notifications are received."</string>
<string name="banner_battery_optimization_submit_android">"Disable optimization"</string>
<string name="banner_battery_optimization_title_android">"Notifications not arriving?"</string>
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
<string name="banner_set_up_recovery_title">"Set up recovery to protect your account"</string>

View file

@ -75,6 +75,7 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
@ -712,6 +713,7 @@ class RoomListPresenterTest {
analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
batteryOptimizationPresenter = { aBatteryOptimizationState() },
notificationCleaner = notificationCleaner,
logoutPresenter = { aDirectLogoutState() },
appPreferencesStore = appPreferencesStore,

View file

@ -5,8 +5,6 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.roomlist.impl
import androidx.activity.ComponentActivity
@ -30,7 +28,6 @@ import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.setSafeContent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule

View file

@ -7,7 +7,6 @@
package io.element.android.features.securebackup.impl.enter
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
@ -71,7 +70,7 @@ fun SecureBackupEnterRecoveryKeyView(
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Content(
state: SecureBackupEnterRecoveryKeyState,

View file

@ -22,7 +22,6 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.alpha
@ -169,7 +168,6 @@ private fun RecoveryKeyWithCopy(
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun RecoveryKeyFormContent(
state: RecoveryKeyViewState,