Merge branch 'develop' into feature/fga/permalink_timeline
This commit is contained in:
commit
2c8abbed0c
1157 changed files with 4307 additions and 1899 deletions
|
|
@ -27,7 +27,7 @@ fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
|
|||
Text(
|
||||
modifier = modifier,
|
||||
text = subtitle,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,17 +18,23 @@ package io.element.android.libraries.designsystem.atomic.atoms
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun RoomPreviewTitleAtom(title: String, modifier: Modifier = Modifier) {
|
||||
fun RoomPreviewTitleAtom(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontStyle: FontStyle? = null,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = title,
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
textAlign = TextAlign.Center,
|
||||
fontStyle = fontStyle,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -42,8 +41,7 @@ fun RoomPreviewMembersCountMolecule(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
|
||||
.widthIn(min = 48.dp)
|
||||
.padding(start = 2.dp, end = 6.dp, top = 2.dp, bottom = 2.dp),
|
||||
.padding(start = 2.dp, end = 8.dp, top = 2.dp, bottom = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -36,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
/**
|
||||
* @param modifier Classical modifier.
|
||||
* @param paddingValues padding values to apply to the content.
|
||||
* @param containerColor color of the container. Set to [Color.Transparent] if you provide a background in the [modifier].
|
||||
* @param background optional background component.
|
||||
* @param topBar optional topBar.
|
||||
* @param header optional header.
|
||||
|
|
@ -46,6 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
fun HeaderFooterPage(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(20.dp),
|
||||
containerColor: Color = MaterialTheme.colorScheme.background,
|
||||
background: @Composable () -> Unit = {},
|
||||
topBar: @Composable () -> Unit = {},
|
||||
header: @Composable () -> Unit = {},
|
||||
|
|
@ -55,6 +59,7 @@ fun HeaderFooterPage(
|
|||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = topBar,
|
||||
containerColor = containerColor,
|
||||
) { padding ->
|
||||
Box {
|
||||
background()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.designsystem.background
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RadialGradientShader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
/**
|
||||
* Light gradient background for Join room screens.
|
||||
*/
|
||||
@Composable
|
||||
fun LightGradientBackground(
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.background,
|
||||
firstColor: Color = Color(0x1E0DBD8B),
|
||||
secondColor: Color = Color(0x001273EB),
|
||||
ratio: Float = 642 / 775f,
|
||||
) {
|
||||
Canvas(
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
val biggerDimension = size.width * 1.98f
|
||||
val gradientShaderBrush = ShaderBrush(
|
||||
RadialGradientShader(
|
||||
colors = listOf(firstColor, secondColor),
|
||||
center = size.center.copy(x = size.width * ratio, y = size.height * ratio),
|
||||
radius = biggerDimension / 2f,
|
||||
colorStops = listOf(0f, 0.95f)
|
||||
)
|
||||
)
|
||||
drawRect(backgroundColor, size = size)
|
||||
drawRect(brush = gradientShaderBrush, size = size)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LightGradientBackgroundPreview() = ElementPreview {
|
||||
LightGradientBackground()
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
package io.element.android.libraries.designsystem.background
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.LinearGradientShader
|
|||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.drawWithLayer
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ fun PageTitle(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TitleWithIconFullPreview(@PreviewParameter(BigIconStylePreviewProvider::class) style: BigIcon.Style) {
|
||||
internal fun PageTitleWithIconFullPreview(@PreviewParameter(BigIconStylePreviewProvider::class) style: BigIcon.Style) {
|
||||
ElementPreview {
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
|
|
@ -124,7 +124,7 @@ internal fun TitleWithIconFullPreview(@PreviewParameter(BigIconStylePreviewProvi
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun TitleWithIconMinimalPreview() {
|
||||
internal fun PageTitleWithIconMinimalPreview() {
|
||||
ElementPreview {
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ internal fun AsyncIndicatorView(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AsyncIndicatorView_Loading_Preview() {
|
||||
internal fun AsyncIndicatorLoadingPreview() {
|
||||
ElementPreview {
|
||||
AsyncIndicator.Loading(text = "Loading")
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ internal fun AsyncIndicatorView_Loading_Preview() {
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AsyncIndicatorView_Failed_Preview() {
|
||||
internal fun AsyncIndicatorFailurePreview() {
|
||||
ElementPreview {
|
||||
AsyncIndicator.Failure(text = "Failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ data class AvatarData(
|
|||
val size: AvatarSize,
|
||||
) {
|
||||
val initial by lazy {
|
||||
(name?.takeIf { it.isNotBlank() } ?: id)
|
||||
// For roomIds, use "#" as initial
|
||||
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
|
||||
.let { dn ->
|
||||
var startIndex = 0
|
||||
val initial = dn[startIndex]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UserAvatarPreview() = ElementPreview {
|
||||
internal fun UserAvatarColorsPreview() = ElementPreview {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ internal fun GradientFloatingActionButtonPreview() {
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun GradientSendButtonPreview() {
|
||||
internal fun GradientFloatingActionButtonCircleShapePreview() {
|
||||
ElementPreview {
|
||||
Box(modifier = Modifier.padding(20.dp)) {
|
||||
GradientFloatingActionButton(
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ private fun PreferenceTopAppBar(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PreferenceViewPreview() = ElementPreview {
|
||||
internal fun PreferencePagePreview() = ElementPreview {
|
||||
PreferencePage(
|
||||
title = "Preference screen",
|
||||
onBackPressed = {},
|
||||
|
|
|
|||
|
|
@ -98,11 +98,11 @@ private fun TextFieldDialog(
|
|||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length)))
|
||||
}
|
||||
var error by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var canRequestFocus by rememberSaveable { mutableStateOf(false) }
|
||||
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
|
||||
ListDialog(
|
||||
title = title,
|
||||
|
|
@ -128,10 +128,11 @@ private fun TextFieldDialog(
|
|||
maxLines = maxLines,
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
)
|
||||
canRequestFocus = true
|
||||
}
|
||||
}
|
||||
|
||||
if (autoSelectOnDisplay) {
|
||||
if (autoSelectOnDisplay && canRequestFocus) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ fun WithRulers(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun WithRulerPreview() = ElementPreview {
|
||||
internal fun WithRulersPreview() = ElementPreview {
|
||||
WithRulers(xRulersOffset = 20.dp, yRulersOffset = 15.dp) {
|
||||
OutlinedButton(
|
||||
text = "A Button with rulers on it!",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
|
@ -52,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
|
|
@ -72,15 +72,13 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
|||
|
||||
override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
// Note: we do not use disambiguated display name here, see
|
||||
// https://github.com/element-hq/element-x-ios/issues/1845#issuecomment-1888707428
|
||||
val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom)
|
||||
is MessageContent -> processMessageContents(content, senderDisambiguatedDisplayName, isDmRoom)
|
||||
RedactedContent -> {
|
||||
val message = sp.getString(CommonStrings.common_message_removed)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
prefix(message, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
|
|
@ -91,36 +89,40 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
|||
is UnableToDecryptContent -> {
|
||||
val message = sp.getString(CommonStrings.common_waiting_for_decryption_key)
|
||||
if (!isDmRoom) {
|
||||
prefix(message, senderDisplayName)
|
||||
prefix(message, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.RoomList)
|
||||
}
|
||||
is PollContent -> {
|
||||
val message = sp.getString(CommonStrings.common_poll_summary, content.question)
|
||||
prefixIfNeeded(message, senderDisplayName, isDmRoom)
|
||||
prefixIfNeeded(message, senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> {
|
||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisplayName, isDmRoom)
|
||||
prefixIfNeeded(sp.getString(CommonStrings.common_unsupported_event), senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite)
|
||||
}?.take(MAX_SAFE_LENGTH)
|
||||
}
|
||||
|
||||
private fun processMessageContents(messageContent: MessageContent, senderDisplayName: String, isDmRoom: Boolean): CharSequence? {
|
||||
private fun processMessageContents(
|
||||
messageContent: MessageContent,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
isDmRoom: Boolean,
|
||||
): CharSequence {
|
||||
val internalMessage = when (val messageType: MessageType = messageContent.type) {
|
||||
// Doesn't need a prefix
|
||||
is EmoteMessageType -> {
|
||||
return "* $senderDisplayName ${messageType.body}"
|
||||
return "* $senderDisambiguatedDisplayName ${messageType.body}"
|
||||
}
|
||||
is TextMessageType -> {
|
||||
messageType.toPlainText(permalinkParser)
|
||||
|
|
@ -153,19 +155,23 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
|
|||
messageType.body
|
||||
}
|
||||
}
|
||||
return prefixIfNeeded(internalMessage, senderDisplayName, isDmRoom)
|
||||
return prefixIfNeeded(internalMessage, senderDisambiguatedDisplayName, isDmRoom)
|
||||
}
|
||||
|
||||
private fun prefixIfNeeded(message: String, senderDisplayName: String, isDmRoom: Boolean): CharSequence = if (isDmRoom) {
|
||||
private fun prefixIfNeeded(
|
||||
message: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
isDmRoom: Boolean,
|
||||
): CharSequence = if (isDmRoom) {
|
||||
message
|
||||
} else {
|
||||
prefix(message, senderDisplayName)
|
||||
prefix(message, senderDisambiguatedDisplayName)
|
||||
}
|
||||
|
||||
private fun prefix(message: String, senderDisplayName: String): AnnotatedString {
|
||||
private fun prefix(message: String, senderDisambiguatedDisplayName: String): AnnotatedString {
|
||||
return buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(senderDisplayName)
|
||||
append(senderDisambiguatedDisplayName)
|
||||
}
|
||||
append(": ")
|
||||
append(message)
|
||||
|
|
|
|||
|
|
@ -49,16 +49,16 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
|||
) : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
val senderDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisplayName, isOutgoing)
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
}
|
||||
is LegacyCallInviteContent -> {
|
||||
sp.getString(CommonStrings.common_call_invite)
|
||||
|
|
|
|||
|
|
@ -27,14 +27,19 @@ class ProfileChangeContentFormatter @Inject constructor(
|
|||
fun format(
|
||||
profileChangeContent: ProfileChangeContent,
|
||||
senderId: UserId,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): String? = profileChangeContent.run {
|
||||
val displayNameChanged = displayName != prevDisplayName
|
||||
val avatarChanged = avatarUrl != prevAvatarUrl
|
||||
return when {
|
||||
avatarChanged && displayNameChanged -> {
|
||||
val message = format(profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null), senderId, senderDisplayName, senderIsYou)
|
||||
val message = format(
|
||||
profileChangeContent = profileChangeContent.copy(avatarUrl = null, prevAvatarUrl = null),
|
||||
senderId = senderId,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
senderIsYou = senderIsYou,
|
||||
)
|
||||
val avatarChangedToo = sp.getString(R.string.state_event_avatar_changed_too)
|
||||
"$message\n$avatarChangedToo"
|
||||
}
|
||||
|
|
@ -63,7 +68,7 @@ class ProfileChangeContentFormatter @Inject constructor(
|
|||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_avatar_url_changed_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_avatar_url_changed, senderDisplayName)
|
||||
sp.getString(R.string.state_event_avatar_url_changed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class RoomMembershipContentFormatter @Inject constructor(
|
|||
) {
|
||||
fun format(
|
||||
membershipContent: RoomMembershipContent,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
): CharSequence? {
|
||||
val userId = membershipContent.userId
|
||||
|
|
@ -38,34 +38,34 @@ class RoomMembershipContentFormatter @Inject constructor(
|
|||
MembershipChange.JOINED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_join_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_join, userId.value)
|
||||
sp.getString(R.string.state_event_room_join, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.LEFT -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_leave_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_leave, userId.value)
|
||||
sp.getString(R.string.state_event_room_leave, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.BANNED, MembershipChange.KICKED_AND_BANNED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_ban_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_ban, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_ban, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.UNBANNED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_unban_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_unban, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_unban, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KICKED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_remove_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_remove, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_remove, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_you, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_invite_you, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_invite, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_invite, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.INVITATION_ACCEPTED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_invite_accepted_by_you)
|
||||
|
|
@ -80,34 +80,34 @@ class RoomMembershipContentFormatter @Inject constructor(
|
|||
MembershipChange.INVITATION_REVOKED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_third_party_revoked_invite, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCKED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock, userId.value)
|
||||
sp.getString(R.string.state_event_room_knock, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.KNOCK_ACCEPTED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_accepted_by_you, userId.value)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_accepted, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_knock_accepted, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.KNOCK_RETRACTED -> if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_retracted_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_retracted, userId.value)
|
||||
sp.getString(R.string.state_event_room_knock_retracted, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.KNOCK_DENIED -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_denied_by_you, userId.value)
|
||||
} else if (memberIsYou) {
|
||||
sp.getString(R.string.state_event_room_knock_denied_you, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_knock_denied_you, senderDisambiguatedDisplayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_knock_denied, senderDisplayName, userId.value)
|
||||
sp.getString(R.string.state_event_room_knock_denied, senderDisambiguatedDisplayName, userId.value)
|
||||
}
|
||||
MembershipChange.NONE -> if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_none_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_none, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_none, senderDisambiguatedDisplayName)
|
||||
}
|
||||
MembershipChange.ERROR -> {
|
||||
Timber.v("Filtering timeline item for room membership: $membershipContent")
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class StateContentFormatter @Inject constructor(
|
|||
) {
|
||||
fun format(
|
||||
stateContent: StateContent,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
senderIsYou: Boolean,
|
||||
renderingMode: RenderingMode,
|
||||
): CharSequence? {
|
||||
|
|
@ -39,15 +39,15 @@ class StateContentFormatter @Inject constructor(
|
|||
when {
|
||||
senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed_by_you)
|
||||
senderIsYou && !hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_removed_by_you)
|
||||
!senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisplayName)
|
||||
else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisplayName)
|
||||
!senderIsYou && hasAvatarUrl -> sp.getString(R.string.state_event_room_avatar_changed, senderDisambiguatedDisplayName)
|
||||
else -> sp.getString(R.string.state_event_room_avatar_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomCreate -> {
|
||||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_created_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_created, senderDisplayName)
|
||||
sp.getString(R.string.state_event_room_created, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomEncryption -> sp.getString(CommonStrings.common_encryption_enabled)
|
||||
|
|
@ -56,8 +56,8 @@ class StateContentFormatter @Inject constructor(
|
|||
when {
|
||||
senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed_by_you, content.name)
|
||||
senderIsYou && !hasRoomName -> sp.getString(R.string.state_event_room_name_removed_by_you)
|
||||
!senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisplayName, content.name)
|
||||
else -> sp.getString(R.string.state_event_room_name_removed, senderDisplayName)
|
||||
!senderIsYou && hasRoomName -> sp.getString(R.string.state_event_room_name_changed, senderDisambiguatedDisplayName, content.name)
|
||||
else -> sp.getString(R.string.state_event_room_name_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomThirdPartyInvite -> {
|
||||
|
|
@ -68,7 +68,7 @@ class StateContentFormatter @Inject constructor(
|
|||
if (senderIsYou) {
|
||||
sp.getString(R.string.state_event_room_third_party_invite_by_you, content.displayName)
|
||||
} else {
|
||||
sp.getString(R.string.state_event_room_third_party_invite, senderDisplayName, content.displayName)
|
||||
sp.getString(R.string.state_event_room_third_party_invite, senderDisambiguatedDisplayName, content.displayName)
|
||||
}
|
||||
}
|
||||
is OtherState.RoomTopic -> {
|
||||
|
|
@ -76,8 +76,8 @@ class StateContentFormatter @Inject constructor(
|
|||
when {
|
||||
senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed_by_you, content.topic)
|
||||
senderIsYou && !hasRoomTopic -> sp.getString(R.string.state_event_room_topic_removed_by_you)
|
||||
!senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisplayName, content.topic)
|
||||
else -> sp.getString(R.string.state_event_room_topic_removed, senderDisplayName)
|
||||
!senderIsYou && hasRoomTopic -> sp.getString(R.string.state_event_room_topic_changed, senderDisambiguatedDisplayName, content.topic)
|
||||
else -> sp.getString(R.string.state_event_room_topic_removed, senderDisambiguatedDisplayName)
|
||||
}
|
||||
}
|
||||
is OtherState.Custom -> when (renderingMode) {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
<string name="state_event_avatar_url_changed_by_you">"Вы змянілі свой аватар"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s быў паніжаны(-на) да ўдзельніка"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s быў паніжаны(-на) да мадэратара"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s змяніў(-ла) сваё адлюстраванае імя з %2$s на %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Вы змянілі сваё адлюстраванае імя з %1$s на %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s выдаліў(-ла) сваё адлюстраванае імя (яно было %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Вы выдалілі сваё адлюстраванае імя (яно было %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s усталявалі сваё адлюстраванае імя на %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Вы ўстанавілі адлюстраванае імя на %1$s"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s змяніў(-ла) сваё бачнае імя з %2$s на %3$s"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Вы змянілі сваё бачнае імя з %1$s на %2$s"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s выдаліў(-ла) сваё бачнае імя (яно было %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Вы выдалілі сваё бачнае імя (яно было %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s усталявалі сваё бачнае імя на %2$s"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Вы ўстанавілі бачнае імя на %1$s"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s быў(-ла) павышаны(-на) да адміністратара"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s быў(-ла) павышаны(-на) да мадэратара"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s змяніў(-ла) аватар пакоя"</string>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
<string name="state_event_room_join_by_you">"You joined the room"</string>
|
||||
<string name="state_event_room_knock">"%1$s requested to join"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s allowed %2$s to join"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s allowed you to join"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"You allowed %1$s to join"</string>
|
||||
<string name="state_event_room_knock_by_you">"You requested to join"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s rejected %2$s\'s request to join"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"You rejected %1$s\'s request to join"</string>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
|
@ -54,6 +53,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import org.junit.Before
|
||||
|
|
@ -264,7 +264,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
val someoneJoinedRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneJoinedRoom = formatter.format(someoneJoinedRoomEvent, false)
|
||||
assertThat(someoneJoinedRoom).isEqualTo("${someoneContent.userId} joined the room")
|
||||
assertThat(someoneJoinedRoom).isEqualTo("$otherName joined the room")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -280,7 +280,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
val someoneLeftRoomEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneLeftRoom = formatter.format(someoneLeftRoomEvent, false)
|
||||
assertThat(someoneLeftRoom).isEqualTo("${someoneContent.userId} left the room")
|
||||
assertThat(someoneLeftRoom).isEqualTo("$otherName left the room")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -421,7 +421,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
val someoneKnockedEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneKnocked = formatter.format(someoneKnockedEvent, false)
|
||||
assertThat(someoneKnocked).isEqualTo("${someoneContent.userId} requested to join")
|
||||
assertThat(someoneKnocked).isEqualTo("$otherName requested to join")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -432,7 +432,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
val youAcceptedKnockEvent = createRoomEvent(sentByYou = true, senderDisplayName = null, content = someoneContent)
|
||||
val youAcceptedKnock = formatter.format(youAcceptedKnockEvent, false)
|
||||
assertThat(youAcceptedKnock).isEqualTo("${someoneContent.userId} allowed you to join")
|
||||
assertThat(youAcceptedKnock).isEqualTo("You allowed ${someoneContent.userId} to join")
|
||||
|
||||
val someoneAcceptedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneAcceptedKnock = formatter.format(someoneAcceptedKnockEvent, false)
|
||||
|
|
@ -452,7 +452,7 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
val someoneRetractedKnockEvent = createRoomEvent(sentByYou = false, senderDisplayName = otherName, content = someoneContent)
|
||||
val someoneRetractedKnock = formatter.format(someoneRetractedKnockEvent, false)
|
||||
assertThat(someoneRetractedKnock).isEqualTo("${someoneContent.userId} is no longer interested in joining")
|
||||
assertThat(someoneRetractedKnock).isEqualTo("$otherName is no longer interested in joining")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -829,9 +829,13 @@ class DefaultRoomLastMessageFormatterTest {
|
|||
|
||||
// endregion
|
||||
|
||||
private fun createRoomEvent(sentByYou: Boolean, senderDisplayName: String?, content: EventContent): EventTimelineItem {
|
||||
private fun createRoomEvent(
|
||||
sentByYou: Boolean,
|
||||
senderDisplayName: String?,
|
||||
content: EventContent,
|
||||
): EventTimelineItem {
|
||||
val sender = if (sentByYou) A_USER_ID else someoneElseId
|
||||
val profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null)
|
||||
val profile = aProfileTimelineDetails(senderDisplayName)
|
||||
return anEventTimelineItem(
|
||||
content = content,
|
||||
senderProfile = profile,
|
||||
|
|
|
|||
|
|
@ -75,13 +75,6 @@ enum class FeatureFlags(
|
|||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
RoomListFilters(
|
||||
key = "feature.roomlistfilters",
|
||||
title = "Room list filters",
|
||||
description = "Allow user to filter the room list",
|
||||
defaultValue = true,
|
||||
isFinished = false,
|
||||
),
|
||||
RoomDirectorySearch(
|
||||
key = "feature.roomdirectorysearch",
|
||||
title = "Room directory search",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.PinUnlock -> true
|
||||
FeatureFlags.Mentions -> true
|
||||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomListFilters -> true
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ interface MatrixClient : Closeable {
|
|||
suspend fun setDisplayName(displayName: String): Result<Unit>
|
||||
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
|
||||
suspend fun removeAvatar(): Result<Unit>
|
||||
suspend fun joinRoom(roomId: RoomId): Result<RoomId>
|
||||
suspend fun joinRoom(roomId: RoomId): Result<Unit>
|
||||
suspend fun knockRoom(roomId: RoomId): Result<Unit>
|
||||
fun syncService(): SyncService
|
||||
fun sessionVerificationService(): SessionVerificationService
|
||||
fun pushersService(): PushersService
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ data class NotificationData(
|
|||
val roomId: RoomId,
|
||||
// mxc url
|
||||
val senderAvatarUrl: String?,
|
||||
// private, must use `getSenderName`
|
||||
// private, must use `getDisambiguatedDisplayName`
|
||||
private val senderDisplayName: String?,
|
||||
private val senderIsNameAmbiguous: Boolean,
|
||||
val roomAvatarUrl: String?,
|
||||
|
|
@ -39,7 +39,7 @@ data class NotificationData(
|
|||
val content: NotificationContent,
|
||||
val hasMention: Boolean,
|
||||
) {
|
||||
fun getSenderName(userId: UserId): String = when {
|
||||
fun getDisambiguatedDisplayName(userId: UserId): String = when {
|
||||
senderDisplayName.isNullOrBlank() -> userId.value
|
||||
senderIsNameAmbiguous -> "$senderDisplayName ($userId)"
|
||||
else -> senderDisplayName
|
||||
|
|
@ -52,6 +52,7 @@ sealed interface NotificationContent {
|
|||
data class CallInvite(
|
||||
val senderId: UserId,
|
||||
) : MessageLike
|
||||
|
||||
data object CallHangup : MessageLike
|
||||
data object CallCandidates : MessageLike
|
||||
data object KeyVerificationReady : MessageLike
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.api.room
|
||||
|
||||
sealed interface RoomType {
|
||||
data object Space : RoomType
|
||||
data object Room : RoomType
|
||||
data class Other(val type: String) : RoomType
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.room.preview
|
|||
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
|
||||
data class RoomPreview(
|
||||
/** The room id for this room. */
|
||||
|
|
@ -33,7 +34,7 @@ data class RoomPreview(
|
|||
/** The number of joined members. */
|
||||
val numberOfJoinedMembers: Long,
|
||||
/** The room type (space, custom) or nothing, if it's a regular room. */
|
||||
val roomType: String?,
|
||||
val roomType: RoomType,
|
||||
/** Is the history world-readable for this room? */
|
||||
val isHistoryWorldReadable: Boolean,
|
||||
/** Is the room joined by the current user? */
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ sealed interface RoomSummary {
|
|||
|
||||
data class RoomSummaryDetails(
|
||||
val roomId: RoomId,
|
||||
val name: String,
|
||||
val name: String?,
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val isDirect: Boolean,
|
||||
val avatarUrl: String?,
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ sealed interface InReplyTo {
|
|||
val eventId: EventId,
|
||||
val content: EventContent,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val senderProfile: ProfileTimelineDetails,
|
||||
) : InReplyTo
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -52,3 +52,10 @@ fun ProfileTimelineDetails.getDisambiguatedDisplayName(userId: UserId): String {
|
|||
else -> userId.value
|
||||
}
|
||||
}
|
||||
|
||||
fun ProfileTimelineDetails.getAvatarUrl(): String? {
|
||||
return when (this) {
|
||||
is ProfileTimelineDetails.Ready -> avatarUrl
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface SessionVerificationService {
|
||||
/**
|
||||
* This flow stores the local verification status of the current session.
|
||||
*
|
||||
* We should ideally base the verified status in the Rust SDK info, but there are several issues with that approach:
|
||||
*
|
||||
* - The SDK takes a while to report this value, resulting in a delay of 1-2s in displaying the UI.
|
||||
* - We need to add a 'Skip' option for testing purposes, which would not be possible if we relied only on the SDK.
|
||||
* - The SDK sometimes doesn't report the verification state if there is no network connection when the app boots.
|
||||
*/
|
||||
val needsVerificationFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* State of the current verification flow ([VerificationFlowState.Initial] if not started).
|
||||
*/
|
||||
|
|
@ -83,11 +72,6 @@ interface SessionVerificationService {
|
|||
* Returns the verification service state to the initial step.
|
||||
*/
|
||||
suspend fun reset()
|
||||
|
||||
/**
|
||||
* Saves the current session state as [verified].
|
||||
*/
|
||||
suspend fun saveVerifiedState(verified: Boolean)
|
||||
}
|
||||
|
||||
/** Verification status of the current session. */
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class NotificationDataTest {
|
|||
senderDisplayName = null,
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("@alice:server.org")
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("@alice:server.org")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -38,7 +38,7 @@ class NotificationDataTest {
|
|||
senderDisplayName = "Alice",
|
||||
senderIsNameAmbiguous = false,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice")
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -47,7 +47,7 @@ class NotificationDataTest {
|
|||
senderDisplayName = "Alice",
|
||||
senderIsNameAmbiguous = true,
|
||||
)
|
||||
assertThat(sut.getSenderName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)")
|
||||
assertThat(sut.getDisambiguatedDisplayName(A_USER_ID)).isEqualTo("Alice (@alice:server.org)")
|
||||
}
|
||||
|
||||
private fun aNotificationData(
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
|||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.room.map
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewMapper
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
||||
|
|
@ -159,7 +160,6 @@ class RustMatrixClient(
|
|||
syncService = rustSyncService,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
dispatchers = dispatchers,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
||||
private val roomDirectoryService = RustRoomDirectoryService(
|
||||
|
|
@ -187,7 +187,6 @@ class RustMatrixClient(
|
|||
isTokenValid = false,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
needsVerification = existingData.needsVerification,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
||||
|
|
@ -215,7 +214,6 @@ class RustMatrixClient(
|
|||
isTokenValid = true,
|
||||
loginType = existingData.loginType,
|
||||
passphrase = existingData.passphrase,
|
||||
needsVerification = existingData.needsVerification,
|
||||
)
|
||||
sessionStore.updateData(newData)
|
||||
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
||||
|
|
@ -241,7 +239,6 @@ class RustMatrixClient(
|
|||
client = client,
|
||||
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
||||
private val eventFilters = TimelineConfig.excludedEvents
|
||||
|
|
@ -439,7 +436,7 @@ class RustMatrixClient(
|
|||
runCatching { client.removeAvatar() }
|
||||
}
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.joinRoomById(roomId.value).destroy()
|
||||
try {
|
||||
|
|
@ -447,10 +444,13 @@ class RustMatrixClient(
|
|||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
}
|
||||
roomId
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> {
|
||||
return Result.failure(NotImplementedError("Not yet implemented"))
|
||||
}
|
||||
|
||||
override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result<Unit> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.trackRecentlyVisitedRoom(roomId.value)
|
||||
|
|
@ -463,21 +463,15 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result<RoomId> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
// TODO Waiting for SDK to be released
|
||||
throw Exception("Not implemented")
|
||||
// client.resolveRoomAlias(roomAlias.value).let(::RoomId)
|
||||
client.resolveRoomAlias(roomAlias.value).let(::RoomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionThrown")
|
||||
override suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result<RoomPreview> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
// TODO Waiting for SDK to be released
|
||||
throw Exception("Not implemented")
|
||||
// client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
|
||||
client.getRoomPreview(roomIdOrAlias.identifier).let(RoomPreviewMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,6 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
isTokenValid = true,
|
||||
loginType = LoginType.PASSWORD,
|
||||
passphrase = pendingPassphrase,
|
||||
needsVerification = true,
|
||||
)
|
||||
}
|
||||
sessionStore.storeData(sessionData)
|
||||
|
|
@ -187,7 +186,6 @@ class RustMatrixAuthenticationService @Inject constructor(
|
|||
isTokenValid = true,
|
||||
loginType = LoginType.OIDC,
|
||||
passphrase = pendingPassphrase,
|
||||
needsVerification = true,
|
||||
)
|
||||
}
|
||||
pendingOidcAuthenticationData?.close()
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
|
|
@ -49,11 +48,10 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover
|
|||
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
|
||||
|
||||
internal class RustEncryptionService(
|
||||
private val client: Client,
|
||||
client: Client,
|
||||
syncService: RustSyncService,
|
||||
sessionCoroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val sessionStore: SessionStore,
|
||||
) : EncryptionService {
|
||||
private val service: Encryption = client.encryption()
|
||||
|
||||
|
|
@ -188,9 +186,6 @@ internal class RustEncryptionService(
|
|||
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
service.recover(recoveryKey)
|
||||
val existingSession = sessionStore.getSession(client.userId())
|
||||
?: error("Failed to save verification state. No session with id ${client.userId()}")
|
||||
sessionStore.updateData(existingSession.copy(needsVerification = false))
|
||||
}.mapFailure {
|
||||
it.mapRecoveryException()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ internal fun Session.toSessionData(
|
|||
isTokenValid: Boolean,
|
||||
loginType: LoginType,
|
||||
passphrase: String?,
|
||||
needsVerification: Boolean,
|
||||
) = SessionData(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
|
|
@ -38,5 +37,4 @@ internal fun Session.toSessionData(
|
|||
isTokenValid = isTokenValid,
|
||||
loginType = loginType,
|
||||
passphrase = passphrase,
|
||||
needsVerification = needsVerification,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.impl.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
|
||||
fun String?.toRoomType(): RoomType {
|
||||
return when (this) {
|
||||
null -> RoomType.Room
|
||||
"m.space" -> RoomType.Space
|
||||
else -> RoomType.Other(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.impl.room.preview
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
import io.element.android.libraries.matrix.impl.room.toRoomType
|
||||
import org.matrix.rustcomponents.sdk.RoomPreview as RustRoomPreview
|
||||
|
||||
object RoomPreviewMapper {
|
||||
fun map(roomPreview: RustRoomPreview): RoomPreview {
|
||||
return RoomPreview(
|
||||
roomId = RoomId(roomPreview.roomId),
|
||||
canonicalAlias = roomPreview.canonicalAlias?.let(::RoomAlias),
|
||||
name = roomPreview.name,
|
||||
topic = roomPreview.topic,
|
||||
avatarUrl = roomPreview.avatarUrl,
|
||||
numberOfJoinedMembers = roomPreview.numJoinedMembers.toLong(),
|
||||
roomType = roomPreview.roomType.toRoomType(),
|
||||
isHistoryWorldReadable = roomPreview.isHistoryWorldReadable,
|
||||
isJoined = roomPreview.isJoined,
|
||||
isInvited = roomPreview.isInvited,
|
||||
isPublic = roomPreview.isPublic,
|
||||
canKnock = roomPreview.canKnock
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ val RoomListFilter.predicate
|
|||
(roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread)
|
||||
}
|
||||
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true)
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.name.orEmpty().contains(pattern, ignoreCase = true)
|
||||
}
|
||||
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
|
||||
roomSummary.isInvited()
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
|||
}
|
||||
return RoomSummaryDetails(
|
||||
roomId = RoomId(roomInfo.id),
|
||||
name = roomInfo.name ?: roomInfo.id,
|
||||
name = roomInfo.name,
|
||||
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
|
||||
isDirect = roomInfo.isDirect,
|
||||
avatarUrl = roomInfo.avatarUrl,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
|
|||
import io.element.android.libraries.matrix.impl.media.map
|
||||
import org.matrix.rustcomponents.sdk.Message
|
||||
import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.ProfileDetails
|
||||
import org.matrix.rustcomponents.sdk.RepliedToEventDetails
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
||||
|
|
@ -51,13 +50,11 @@ class EventMessageMapper {
|
|||
val inReplyToId = EventId(details.eventId)
|
||||
when (val event = details.event) {
|
||||
is RepliedToEventDetails.Ready -> {
|
||||
val senderProfile = event.senderProfile as? ProfileDetails.Ready
|
||||
InReplyTo.Ready(
|
||||
eventId = inReplyToId,
|
||||
content = timelineEventContentMapper.map(event.content),
|
||||
senderId = UserId(event.sender),
|
||||
senderDisplayName = senderProfile?.displayName,
|
||||
senderAvatarUrl = senderProfile?.avatarUrl,
|
||||
senderProfile = event.senderProfile.map(),
|
||||
)
|
||||
}
|
||||
is RepliedToEventDetails.Error -> InReplyTo.Error
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.verification
|
||||
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
|
|
@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
|
|||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -33,10 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -58,7 +53,6 @@ class RustSessionVerificationService(
|
|||
private val client: Client,
|
||||
isSyncServiceReady: Flow<Boolean>,
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionStore: SessionStore,
|
||||
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||
private val encryptionService: Encryption = client.encryption()
|
||||
private lateinit var verificationController: SessionVerificationController
|
||||
|
|
@ -80,11 +74,6 @@ class RustSessionVerificationService(
|
|||
}
|
||||
})
|
||||
|
||||
override val needsVerificationFlow: StateFlow<Boolean> = sessionStore.sessionsFlow()
|
||||
.map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||
override val verificationFlowState = _verificationFlowState.asStateFlow()
|
||||
|
||||
|
|
@ -98,6 +87,9 @@ class RustSessionVerificationService(
|
|||
}
|
||||
|
||||
init {
|
||||
// Update initial state in case sliding sync isn't ready
|
||||
updateVerificationStatus(encryptionService.verificationState())
|
||||
|
||||
isReady.onEach { isReady ->
|
||||
if (isReady) {
|
||||
Timber.d("Starting verification service")
|
||||
|
|
@ -165,7 +157,6 @@ class RustSessionVerificationService(
|
|||
}
|
||||
}
|
||||
.onSuccess {
|
||||
saveVerifiedState(true)
|
||||
updateVerificationStatus(VerificationState.VERIFIED)
|
||||
_verificationFlowState.value = VerificationFlowState.Finished
|
||||
}
|
||||
|
|
@ -195,14 +186,6 @@ class RustSessionVerificationService(
|
|||
_verificationFlowState.value = VerificationFlowState.Initial
|
||||
}
|
||||
|
||||
override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail {
|
||||
val existingSession = sessionStore.getSession(client.userId())
|
||||
?: error("Failed to save verification state. No session with id ${client.userId()}")
|
||||
sessionStore.updateData(existingSession.copy(needsVerification = !verified))
|
||||
// Wait until the new state is saved
|
||||
needsVerificationFlow.first { needsVerification -> !needsVerification }
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
Timber.d("Destroying RustSessionVerificationService")
|
||||
verificationStateListenerTaskHandle.cancelAndDestroy()
|
||||
|
|
|
|||
|
|
@ -22,13 +22,20 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
|||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.UnableToDecryptInfo
|
||||
import uniffi.matrix_sdk_crypto.UtdCause
|
||||
|
||||
class UtdTrackerTest {
|
||||
@Test
|
||||
fun `when onUtd is called with null timeToDecryptMs, the expected analytics Event is sent`() {
|
||||
val fakeAnalyticsService = FakeAnalyticsService()
|
||||
val sut = UtdTracker(fakeAnalyticsService)
|
||||
sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = null))
|
||||
sut.onUtd(
|
||||
UnableToDecryptInfo(
|
||||
eventId = AN_EVENT_ID.value,
|
||||
timeToDecryptMs = null,
|
||||
cause = UtdCause.UNKNOWN,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
|
||||
Error(
|
||||
context = null,
|
||||
|
|
@ -47,7 +54,13 @@ class UtdTrackerTest {
|
|||
fun `when onUtd is called with timeToDecryptMs, the expected analytics Event is sent`() {
|
||||
val fakeAnalyticsService = FakeAnalyticsService()
|
||||
val sut = UtdTracker(fakeAnalyticsService)
|
||||
sut.onUtd(UnableToDecryptInfo(eventId = AN_EVENT_ID.value, timeToDecryptMs = 123.toULong()))
|
||||
sut.onUtd(
|
||||
UnableToDecryptInfo(
|
||||
eventId = AN_EVENT_ID.value,
|
||||
timeToDecryptMs = 123.toULong(),
|
||||
cause = UtdCause.UNKNOWN,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
|
||||
Error(
|
||||
context = null,
|
||||
|
|
|
|||
|
|
@ -103,10 +103,12 @@ class FakeMatrixClient(
|
|||
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
||||
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
||||
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
|
||||
var joinRoomLambda: (RoomId) -> Result<RoomId> = {
|
||||
Result.success(it)
|
||||
var joinRoomLambda: (RoomId) -> Result<Unit> = {
|
||||
Result.success(Unit)
|
||||
}
|
||||
var knockRoomLambda: (RoomId) -> Result<Unit> = {
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
var getRoomInfoFlowLambda = { _: RoomId ->
|
||||
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
||||
}
|
||||
|
|
@ -197,7 +199,9 @@ class FakeMatrixClient(
|
|||
return removeAvatarResult
|
||||
}
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = joinRoomLambda(roomId)
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<Unit> = joinRoomLambda(roomId)
|
||||
|
||||
override suspend fun knockRoom(roomId: RoomId): Result<Unit> = knockRoomLambda(roomId)
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ fun aRoomSummaryFilled(
|
|||
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
name: String? = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
|
|
|
|||
|
|
@ -20,22 +20,17 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
|
|||
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.VerificationFlowState
|
||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeSessionVerificationService(
|
||||
var saveVerifiedStateResult: LambdaOneParamRecorder<Boolean, Unit> = lambdaRecorder<Boolean, Unit> {}
|
||||
) : SessionVerificationService {
|
||||
class FakeSessionVerificationService : SessionVerificationService {
|
||||
private val _isReady = MutableStateFlow(false)
|
||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||
private var _canVerifySessionFlow = MutableStateFlow(true)
|
||||
var shouldFail = false
|
||||
|
||||
override val needsVerificationFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
|
||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
||||
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
|
||||
|
|
@ -94,15 +89,7 @@ class FakeSessionVerificationService(
|
|||
_isReady.value = value
|
||||
}
|
||||
|
||||
fun givenNeedsVerification(value: Boolean) {
|
||||
needsVerificationFlow.value = value
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
_verificationFlowState.value = VerificationFlowState.Initial
|
||||
}
|
||||
|
||||
override suspend fun saveVerifiedState(verified: Boolean) {
|
||||
saveVerifiedStateResult(verified)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.core)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
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.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.Text
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||
|
||||
@Composable
|
||||
fun InviteSenderView(
|
||||
inviteSender: InviteSender,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Avatar(avatarData = inviteSender.avatarData)
|
||||
Text(
|
||||
text = inviteSender.annotatedString(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun InviteSenderViewPreview() = ElementPreview {
|
||||
InviteSenderView(
|
||||
inviteSender = InviteSender(
|
||||
userId = UserId("@bob:example.com"),
|
||||
displayName = "Bob",
|
||||
avatarData = AvatarData(
|
||||
id = "@bob:example.com",
|
||||
name = "Bob",
|
||||
url = null,
|
||||
size = AvatarSize.InviteSender
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
|
||||
open class RoomSummaryDetailsProvider : PreviewParameterProvider<RoomSummaryDetails> {
|
||||
override val values: Sequence<RoomSummaryDetails>
|
||||
get() = sequenceOf(
|
||||
aRoomSummaryDetails(),
|
||||
aRoomSummaryDetails(name = null),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = RoomId("!room:domain"),
|
||||
name: String? = "roomName",
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
isDirect: Boolean = true,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = null,
|
||||
inviter: RoomMember? = null,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
numUnreadNotifications: Int = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
isDirect = isDirect,
|
||||
avatarUrl = avatarUrl,
|
||||
lastMessage = lastMessage,
|
||||
inviter = inviter,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDm = isDm,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
isFavorite = isFavorite,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -43,12 +44,6 @@ 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.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
|
|
@ -67,7 +62,8 @@ fun SelectedRoom(
|
|||
) {
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarUrl, AvatarSize.SelectedRoom))
|
||||
Text(
|
||||
text = roomSummary.name,
|
||||
// If name is null, we do not have space to render "No room name", so just use `#` here.
|
||||
text = roomSummary.name ?: "#",
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
|
@ -97,45 +93,11 @@ fun SelectedRoom(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectedRoomPreview() = ElementPreview {
|
||||
internal fun SelectedRoomPreview(
|
||||
@PreviewParameter(RoomSummaryDetailsProvider::class) roomSummaryDetails: RoomSummaryDetails
|
||||
) = ElementPreview {
|
||||
SelectedRoom(
|
||||
roomSummary = aRoomSummaryDetails(),
|
||||
roomSummary = roomSummaryDetails,
|
||||
onRoomRemoved = {},
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomSummaryDetails(
|
||||
roomId: RoomId = RoomId("!room:domain"),
|
||||
name: String = "roomName",
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
isDirect: Boolean = true,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = null,
|
||||
inviter: RoomMember? = null,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
numUnreadNotifications: Int = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
isDirect = isDirect,
|
||||
avatarUrl = avatarUrl,
|
||||
lastMessage = lastMessage,
|
||||
inviter = inviter,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDm = isDm,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
isFavorite = isFavorite,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ fun SelectedUsersRowList(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectedUsersListPreview() = ElementPreview {
|
||||
internal fun SelectedUsersRowListPreview() = ElementPreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// Two users that will be visible with no scrolling
|
||||
SelectedUsersRowList(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.matrix.ui.model
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.ui.R
|
||||
|
||||
@Immutable
|
||||
data class InviteSender(
|
||||
val userId: UserId,
|
||||
val displayName: String,
|
||||
val avatarData: AvatarData,
|
||||
) {
|
||||
@Composable
|
||||
fun annotatedString(): AnnotatedString {
|
||||
return stringResource(R.string.screen_invites_invited_you, displayName, userId.value).let { text ->
|
||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
||||
AnnotatedString(
|
||||
text = text,
|
||||
spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
start = senderNameStart,
|
||||
end = senderNameStart + displayName.length
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RoomMember.toInviteSender() = InviteSender(
|
||||
userId = userId,
|
||||
displayName = displayName ?: "",
|
||||
avatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.InviteSender,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) запрасіў вас"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ви покани"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval(a)"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) hat dich eingeladen"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) te invitó"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vous a invité(e)"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) meghívta"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) ti ha invitato"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) v-a invitat."</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) пригласил вас"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) vás pozval/a"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) bjöd in dig"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) запросив (-ла) Вас"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s(%2$s)邀請您"</string>
|
||||
</resources>
|
||||
4
libraries/matrixui/src/main/res/values/localazy.xml
Normal file
4
libraries/matrixui/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||
</resources>
|
||||
|
|
@ -34,5 +34,8 @@ interface SessionPreferencesStore {
|
|||
suspend fun setRenderTypingNotifications(enabled: Boolean)
|
||||
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
|
||||
|
||||
suspend fun setSkipSessionVerification(skip: Boolean)
|
||||
fun isSessionVerificationSkipped(): Flow<Boolean>
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
class DefaultSessionPreferencesStore(
|
||||
|
|
@ -51,9 +49,18 @@ class DefaultSessionPreferencesStore(
|
|||
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
|
||||
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
||||
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
||||
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
||||
|
||||
private val dataStoreFile = storeFile(context, sessionId)
|
||||
private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile }
|
||||
private val store = PreferenceDataStoreFactory.create(
|
||||
scope = sessionCoroutineScope,
|
||||
migrations = listOf(
|
||||
SessionPreferencesStoreMigration(
|
||||
sharePresenceKey,
|
||||
sendPublicReadReceiptsKey,
|
||||
)
|
||||
),
|
||||
) { dataStoreFile }
|
||||
|
||||
override suspend fun setSharePresence(enabled: Boolean) {
|
||||
update(sharePresenceKey, enabled)
|
||||
|
|
@ -65,8 +72,7 @@ class DefaultSessionPreferencesStore(
|
|||
}
|
||||
|
||||
override fun isSharePresenceEnabled(): Flow<Boolean> {
|
||||
// Migration, if sendPublicReadReceiptsKey was false, consider that sharing presence is false.
|
||||
return get(sharePresenceKey) { runBlocking { isSendPublicReadReceiptsEnabled().first() } }
|
||||
return get(sharePresenceKey) { true }
|
||||
}
|
||||
|
||||
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
|
||||
|
|
@ -81,6 +87,9 @@ class DefaultSessionPreferencesStore(
|
|||
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
|
||||
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
|
||||
|
||||
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
||||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) 2024 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.libraries.preferences.impl.store
|
||||
|
||||
import androidx.datastore.core.DataMigration
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
|
||||
class SessionPreferencesStoreMigration(
|
||||
private val sharePresenceKey: Preferences.Key<Boolean>,
|
||||
private val sendPublicReadReceiptsKey: Preferences.Key<Boolean>,
|
||||
) : DataMigration<Preferences> {
|
||||
override suspend fun cleanUp() = Unit
|
||||
|
||||
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
|
||||
return currentData[sharePresenceKey] == null
|
||||
}
|
||||
|
||||
override suspend fun migrate(currentData: Preferences): Preferences {
|
||||
// If sendPublicReadReceiptsKey was false, consider that sharing presence is false.
|
||||
val defaultValue = currentData[sendPublicReadReceiptsKey] ?: true
|
||||
return currentData.toMutablePreferences().apply {
|
||||
set(sharePresenceKey, defaultValue)
|
||||
}.toPreferences()
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,14 @@ class InMemorySessionPreferencesStore(
|
|||
isRenderReadReceiptsEnabled: Boolean = true,
|
||||
isSendTypingNotificationsEnabled: Boolean = true,
|
||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||
isSessionVerificationSkipped: Boolean = false,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
|
||||
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
||||
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
||||
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
||||
var clearCallCount = 0
|
||||
private set
|
||||
|
||||
|
|
@ -65,6 +67,14 @@ class InMemorySessionPreferencesStore(
|
|||
|
||||
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
|
||||
|
||||
override suspend fun setSkipSessionVerification(skip: Boolean) {
|
||||
isSessionVerificationSkipped.tryEmit(skip)
|
||||
}
|
||||
|
||||
override fun isSessionVerificationSkipped(): Flow<Boolean> {
|
||||
return isSessionVerificationSkipped
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
clearCallCount++
|
||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class NotifiableEventResolver @Inject constructor(
|
|||
): NotifiableEvent? {
|
||||
return when (val content = this.content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val senderName = getSenderName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderName)
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||
val notificationBody = if (hasMention) {
|
||||
stringProvider.getString(R.string.notification_mentioned_you_body, messageBody)
|
||||
} else {
|
||||
|
|
@ -107,7 +107,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = senderName,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
body = notificationBody,
|
||||
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||
roomName = roomDisplayName,
|
||||
|
|
@ -154,7 +154,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = null,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = stringProvider.getString(CommonStrings.common_call_invite),
|
||||
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||
roomName = roomDisplayName,
|
||||
|
|
@ -180,7 +180,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||
eventId = eventId,
|
||||
noisy = isNoisy,
|
||||
timestamp = this.timestamp,
|
||||
senderName = getSenderName(content.senderId),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = stringProvider.getString(CommonStrings.common_poll_summary, content.question),
|
||||
imageUriString = null,
|
||||
roomName = roomDisplayName,
|
||||
|
|
@ -244,12 +244,12 @@ class NotifiableEventResolver @Inject constructor(
|
|||
|
||||
private fun descriptionFromMessageContent(
|
||||
content: NotificationContent.MessageLike.RoomMessage,
|
||||
senderDisplayName: String,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
): String {
|
||||
return when (val messageType = content.messageType) {
|
||||
is AudioMessageType -> messageType.body
|
||||
is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message)
|
||||
is EmoteMessageType -> "* $senderDisplayName ${messageType.body}"
|
||||
is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}"
|
||||
is FileMessageType -> messageType.body
|
||||
is ImageMessageType -> messageType.body
|
||||
is StickerMessageType -> messageType.body
|
||||
|
|
@ -310,7 +310,7 @@ private fun buildNotifiableMessageEvent(
|
|||
canBeReplaced: Boolean = false,
|
||||
noisy: Boolean,
|
||||
timestamp: Long,
|
||||
senderName: String?,
|
||||
senderDisambiguatedDisplayName: String?,
|
||||
body: String?,
|
||||
// We cannot use Uri? type here, as that could trigger a
|
||||
// NotSerializableException when persisting this to storage
|
||||
|
|
@ -335,7 +335,7 @@ private fun buildNotifiableMessageEvent(
|
|||
canBeReplaced = canBeReplaced,
|
||||
noisy = noisy,
|
||||
timestamp = timestamp,
|
||||
senderName = senderName,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
body = body,
|
||||
imageUriString = imageUriString,
|
||||
threadId = threadId,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
imageLoader: ImageLoader,
|
||||
): RoomNotification.Message {
|
||||
val lastKnownRoomEvent = events.last()
|
||||
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
|
||||
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)"
|
||||
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
||||
val style = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
|
|
@ -60,9 +60,9 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
|
||||
val tickerText = if (roomIsGroup) {
|
||||
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
|
||||
stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderDisambiguatedDisplayName, events.last().description)
|
||||
} else {
|
||||
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
|
||||
stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderDisambiguatedDisplayName, events.last().description)
|
||||
}
|
||||
|
||||
val largeBitmap = getRoomBitmap(events, imageLoader)
|
||||
|
|
@ -108,7 +108,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
null
|
||||
} else {
|
||||
Person.Builder()
|
||||
.setName(event.senderName?.annotateForDebug(70))
|
||||
.setName(event.senderDisambiguatedDisplayName?.annotateForDebug(70))
|
||||
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath, imageLoader))
|
||||
.setKey(event.senderId.value)
|
||||
.build()
|
||||
|
|
@ -152,7 +152,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return if (roomIsDirect) {
|
||||
buildSpannedString {
|
||||
event.senderName?.let {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(it)
|
||||
append(": ")
|
||||
|
|
@ -165,7 +165,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(roomName)
|
||||
append(": ")
|
||||
event.senderName?.let {
|
||||
event.senderDisambiguatedDisplayName?.let {
|
||||
append(it)
|
||||
append(" ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ data class NotifiableMessageEvent(
|
|||
val senderId: UserId,
|
||||
val noisy: Boolean,
|
||||
val timestamp: Long,
|
||||
val senderName: String?,
|
||||
val senderDisambiguatedDisplayName: String?,
|
||||
val body: String?,
|
||||
// We cannot use Uri? type here, as that could trigger a
|
||||
// NotSerializableException when persisting this to storage
|
||||
|
|
@ -55,7 +55,6 @@ data class NotifiableMessageEvent(
|
|||
) : NotifiableEvent {
|
||||
val type: String = EventType.MESSAGE
|
||||
override val description: String = body ?: ""
|
||||
val title: String = senderName ?: ""
|
||||
|
||||
// Example of value:
|
||||
// content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ class NotifiableEventResolverTest {
|
|||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderName = null,
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = "Call in progress (unsupported)",
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
|
|
@ -586,7 +586,7 @@ class NotifiableEventResolverTest {
|
|||
senderId = A_USER_ID_2,
|
||||
noisy = false,
|
||||
timestamp = A_TIMESTAMP,
|
||||
senderName = "Bob",
|
||||
senderDisambiguatedDisplayName = "Bob",
|
||||
body = body,
|
||||
imageUriString = null,
|
||||
threadId = null,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ fun aNotifiableMessageEvent(
|
|||
editedEventId = null,
|
||||
noisy = false,
|
||||
timestamp = timestamp,
|
||||
senderName = "sender-name",
|
||||
senderDisambiguatedDisplayName = "sender-name",
|
||||
senderId = UserId("@sending-id:domain.com"),
|
||||
body = "message-body",
|
||||
roomId = roomId,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class RoomSelectPresenter @AssistedInject constructor(
|
|||
LaunchedEffect(query, summaries) {
|
||||
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()
|
||||
.map { it.details }
|
||||
.filter { it.name.contains(query, ignoreCase = true) }
|
||||
.filter { it.name.orEmpty().contains(query, ignoreCase = true) }
|
||||
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
|
||||
.toPersistentList()
|
||||
results = if (filteredSummaries.isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -68,4 +68,8 @@ private fun aForwardMessagesRoomList() = persistentListOf(
|
|||
name = "Room with alias",
|
||||
canonicalAlias = RoomAlias("#alias:example.org"),
|
||||
),
|
||||
aRoomSummaryDetails(
|
||||
roomId = RoomId("!room3:domain"),
|
||||
name = null,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -235,7 +236,8 @@ private fun RoomSummaryView(
|
|||
// Name
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
text = summary.name,
|
||||
text = summary.name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { summary.name == null },
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
|
|
|||
|
|
@ -44,6 +44,4 @@ data class SessionData(
|
|||
val loginType: LoginType,
|
||||
/** The optional passphrase used to encrypt data in the SDK local store. */
|
||||
val passphrase: String?,
|
||||
/** Whether the session needs verification. */
|
||||
val needsVerification: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
|||
isTokenValid = if (isTokenValid) 1L else 0L,
|
||||
loginType = loginType.name,
|
||||
passphrase = passphrase,
|
||||
needsVerification = if (needsVerification) 1L else 0L,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +50,5 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
|||
isTokenValid = isTokenValid == 1L,
|
||||
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
||||
passphrase = passphrase,
|
||||
needsVerification = needsVerification == 1L,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -23,9 +23,7 @@ CREATE TABLE SessionData (
|
|||
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||
loginType TEXT,
|
||||
-- added in version 5
|
||||
passphrase TEXT,
|
||||
-- added in version 6
|
||||
needsVerification INTEGER NOT NULL DEFAULT 0
|
||||
passphrase TEXT
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
-- Migrate DB from version 6
|
||||
-- Remove DB value for verified status, we're back to using the Rust SDK as a source of truth
|
||||
|
||||
CREATE TABLE SessionData_bak (
|
||||
userId TEXT NOT NULL PRIMARY KEY,
|
||||
deviceId TEXT NOT NULL,
|
||||
accessToken TEXT NOT NULL,
|
||||
refreshToken TEXT,
|
||||
homeserverUrl TEXT NOT NULL,
|
||||
slidingSyncProxy TEXT,
|
||||
loginTimestamp INTEGER,
|
||||
oidcData TEXT,
|
||||
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||
loginType TEXT,
|
||||
passphrase TEXT
|
||||
);
|
||||
|
||||
INSERT INTO SessionData_bak SELECT userId, deviceId, accessToken, refreshToken, homeserverUrl, slidingSyncProxy, loginTimestamp, oidcData, isTokenValid, loginType, passphrase FROM SessionData;
|
||||
DROP TABLE SessionData;
|
||||
ALTER TABLE SessionData_bak RENAME TO SessionData;
|
||||
|
|
@ -144,7 +144,6 @@ class DatabaseSessionStoreTests {
|
|||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
passphrase = "aPassphrase",
|
||||
needsVerification = 1L,
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userId",
|
||||
|
|
@ -158,7 +157,6 @@ class DatabaseSessionStoreTests {
|
|||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
passphrase = "aPassphraseAltered",
|
||||
needsVerification = 0L,
|
||||
)
|
||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||
|
|
@ -179,7 +177,6 @@ class DatabaseSessionStoreTests {
|
|||
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||
assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
|
||||
assertThat(alteredSession.needsVerification).isEqualTo(secondSessionData.needsVerification)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -196,7 +193,6 @@ class DatabaseSessionStoreTests {
|
|||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
passphrase = "aPassphrase",
|
||||
needsVerification = 1L,
|
||||
)
|
||||
val secondSessionData = SessionData(
|
||||
userId = "userIdUnknown",
|
||||
|
|
@ -210,7 +206,6 @@ class DatabaseSessionStoreTests {
|
|||
isTokenValid = 1,
|
||||
loginType = null,
|
||||
passphrase = "aPassphraseAltered",
|
||||
needsVerification = 0L,
|
||||
)
|
||||
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
||||
|
||||
|
|
@ -229,6 +224,5 @@ class DatabaseSessionStoreTests {
|
|||
assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||
assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData)
|
||||
assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase)
|
||||
assertThat(notAlteredSession.needsVerification).isEqualTo(firstSessionData.needsVerification)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,4 @@ internal fun aSessionData() = SessionData(
|
|||
isTokenValid = 1,
|
||||
loginType = LoginType.UNKNOWN.name,
|
||||
passphrase = null,
|
||||
needsVerification = 0L,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -566,6 +566,8 @@ private fun ReplyToModeView(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
color = ElementTheme.materialColors.primary,
|
||||
|
|
@ -731,7 +733,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
|
|||
voiceMessageState = VoiceMessageState.Idle,
|
||||
composerMode = MessageComposerMode.Reply(
|
||||
isThreaded = true,
|
||||
senderName = "Alice",
|
||||
senderName = "Alice with a very long name to test overflow in the composer",
|
||||
eventId = EventId("$1234"),
|
||||
attachmentThumbnailInfo = null,
|
||||
defaultContent = "A message\n" +
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ internal fun FormattingOption(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun FormattingButtonPreview() = ElementPreview {
|
||||
internal fun FormattingOptionPreview() = ElementPreview {
|
||||
Row {
|
||||
FormattingOption(
|
||||
state = FormattingOptionState.Default,
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@
|
|||
<string name="common_offline">"Па-за сеткай"</string>
|
||||
<string name="common_or">"або"</string>
|
||||
<string name="common_password">"Пароль"</string>
|
||||
<string name="common_people">"Удзельнікі"</string>
|
||||
<string name="common_people">"Людзі"</string>
|
||||
<string name="common_permalink">"Пастаянная спасылка"</string>
|
||||
<string name="common_permission">"Дазвол"</string>
|
||||
<string name="common_poll_end_confirmation">"Вы ўпэўнены, што хочаце скончыць гэтае апытанне?"</string>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@
|
|||
<string name="common_settings">"Pengaturan"</string>
|
||||
<string name="common_shared_location">"Lokasi terbagi"</string>
|
||||
<string name="common_signing_out">"Mengeluarkan dari akun"</string>
|
||||
<string name="common_something_went_wrong">"Ada yang salah"</string>
|
||||
<string name="common_starting_chat">"Memulai obrolan…"</string>
|
||||
<string name="common_sticker">"Stiker"</string>
|
||||
<string name="common_success">"Berhasil"</string>
|
||||
|
|
|
|||
|
|
@ -158,12 +158,14 @@
|
|||
<string name="common_modern">"Modern"</string>
|
||||
<string name="common_mute">"Mute"</string>
|
||||
<string name="common_no_results">"No results"</string>
|
||||
<string name="common_no_room_name">"No room name"</string>
|
||||
<string name="common_offline">"Offline"</string>
|
||||
<string name="common_or">"or"</string>
|
||||
<string name="common_password">"Password"</string>
|
||||
<string name="common_people">"People"</string>
|
||||
<string name="common_permalink">"Permalink"</string>
|
||||
<string name="common_permission">"Permission"</string>
|
||||
<string name="common_please_wait">"Please wait…"</string>
|
||||
<string name="common_poll_end_confirmation">"Are you sure you want to end this poll?"</string>
|
||||
<string name="common_poll_summary">"Poll: %1$s"</string>
|
||||
<string name="common_poll_total_votes">"Total votes: %1$s"</string>
|
||||
|
|
@ -213,6 +215,7 @@
|
|||
<string name="common_topic">"Topic"</string>
|
||||
<string name="common_topic_placeholder">"What is this room about?"</string>
|
||||
<string name="common_unable_to_decrypt">"Unable to decrypt"</string>
|
||||
<string name="common_unable_to_decrypt_no_access">"You don\'t have access to this message"</string>
|
||||
<string name="common_unable_to_invite_message">"Invites couldn\'t be sent to one or more users."</string>
|
||||
<string name="common_unable_to_invite_title">"Unable to send invite(s)"</string>
|
||||
<string name="common_unlock">"Unlock"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue