Merge branch 'develop' into julioromano/poll_history_entry_point

This commit is contained in:
ganfra 2023-12-14 15:17:13 +01:00
commit 5ac3f273ea
257 changed files with 916 additions and 1161 deletions

View file

@ -27,18 +27,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.preview.debugPlaceholderAvatar
import io.element.android.libraries.designsystem.text.toSp
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.compound.theme.ElementTheme
import timber.log.Timber
@Composable
@ -96,7 +97,9 @@ private fun InitialsAvatar(
val ratio = fontSize.value / originalFont.fontSize.value
val lineHeight = originalFont.lineHeight * ratio
Text(
modifier = Modifier.align(Alignment.Center),
modifier = Modifier
.clearAndSetSemantics {}
.align(Alignment.Center),
text = avatarData.initial,
style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
color = avatarColors.foreground,

View file

@ -36,4 +36,11 @@ interface Feature {
* The default value of the feature (enabled or disabled).
*/
val defaultValue: Boolean
/**
* Whether the feature is finished or not.
* If false: the feature is still in development, it will appear in the developer options screen to be able to enable it and test it.
* If true: the feature is finished, it will not appear in the developer options screen.
*/
val isFinished: Boolean
}

View file

@ -25,52 +25,61 @@ enum class FeatureFlags(
override val key: String,
override val title: String,
override val description: String? = null,
override val defaultValue: Boolean
override val defaultValue: Boolean,
override val isFinished: Boolean,
) : Feature {
LocationSharing(
key = "feature.locationsharing",
title = "Allow user to share location",
defaultValue = true,
isFinished = true,
),
Polls(
key = "feature.polls",
title = "Polls",
description = "Create poll and render poll events in the timeline",
defaultValue = true,
isFinished = true,
),
NotificationSettings(
key = "feature.notificationsettings",
title = "Show notification settings",
defaultValue = true,
isFinished = true,
),
VoiceMessages(
key = "feature.voicemessages",
title = "Voice messages",
description = "Send and receive voice messages",
defaultValue = true,
isFinished = true,
),
PinUnlock(
key = "feature.pinunlock",
title = "Pin unlock",
description = "Allow user to lock/unlock the app with a pin code or biometrics",
defaultValue = true,
isFinished = true,
),
Mentions(
key = "feature.mentions",
title = "Mentions",
description = "Type `@` to get mention suggestions and insert them",
defaultValue = false,
isFinished = false,
),
SecureStorage(
key = "feature.securestorage",
title = "Chat backup",
description = "Allow access to backup and restore chat history settings",
defaultValue = false,
isFinished = false,
),
ReadReceipts(
key = "feature.readreceipts",
title = "Show read receipts",
description = null,
defaultValue = false,
isFinished = false,
),
}

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
/**
@ -32,7 +33,15 @@ sealed interface PermalinkData {
val isRoomAlias: Boolean,
val eventId: String?,
val viaParameters: ImmutableList<String>
) : PermalinkData
) : PermalinkData {
fun getRoomId(): RoomId? {
return roomIdOrAlias.takeIf { !isRoomAlias }?.let(::RoomId)
}
fun getRoomAlias(): String? {
return roomIdOrAlias.takeIf { isRoomAlias }
}
}
/*
* &room_name=Team2

View file

@ -16,7 +16,11 @@
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
sealed interface Mention {
data class User(val userId: String): Mention
data class User(val userId: UserId): Mention
data object AtRoom: Mention
data class Room(val roomId: RoomId?, val roomAlias: String?): Mention
}

View file

@ -49,7 +49,8 @@ data class PollContent(
val maxSelections: ULong,
val answers: ImmutableList<PollAnswer>,
val votes: ImmutableMap<String, ImmutableList<UserId>>,
val endTime: ULong?
val endTime: ULong?,
val isEdited: Boolean,
) : EventContent
data class UnableToDecryptContent(

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.permalink
import com.google.common.truth.Truth.assertThat
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
class PermalinkDataTest {
@Test
fun `getRoomId() returns value when isRoomAlias is false`() {
val permalinkData = PermalinkData.RoomLink(
roomIdOrAlias = "!abcdef123456:matrix.org",
isRoomAlias = false,
eventId = null,
viaParameters = persistentListOf(),
)
assertThat(permalinkData.getRoomId()).isNotNull()
assertThat(permalinkData.getRoomAlias()).isNull()
}
@Test
fun `getRoomAlias() returns value when isRoomAlias is true`() {
val permalinkData = PermalinkData.RoomLink(
roomIdOrAlias = "#room:matrix.org",
isRoomAlias = true,
eventId = null,
viaParameters = persistentListOf(),
)
assertThat(permalinkData.getRoomId()).isNull()
assertThat(permalinkData.getRoomAlias()).isNotNull()
}
}

View file

@ -24,11 +24,20 @@ class BackupUploadStateMapper {
return when (rustEnableProgress) {
RustBackupUploadState.Done ->
BackupUploadState.Done
is RustBackupUploadState.Uploading ->
BackupUploadState.Uploading(
backedUpCount = rustEnableProgress.backedUpCount.toInt(),
totalCount = rustEnableProgress.totalCount.toInt(),
)
is RustBackupUploadState.Uploading -> {
val backedUpCount = rustEnableProgress.backedUpCount.toInt()
val totalCount = rustEnableProgress.totalCount.toInt()
if (backedUpCount == totalCount) {
// Consider that the state is Done in this case,
// the SDK will not send a Done state
BackupUploadState.Done
} else {
BackupUploadState.Uploading(
backedUpCount = backedUpCount,
totalCount = totalCount,
)
}
}
RustBackupUploadState.Waiting ->
BackupUploadState.Waiting
RustBackupUploadState.Error ->

View file

@ -21,6 +21,6 @@ import org.matrix.rustcomponents.sdk.Mentions
fun List<Mention>.map(): Mentions {
val hasAtRoom = any { it is Mention.AtRoom }
val userIds = filterIsInstance<Mention.User>().map { it.userId }
val userIds = filterIsInstance<Mention.User>().map { it.userId.value }
return Mentions(userIds, hasAtRoom)
}

View file

@ -113,6 +113,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
vote.value.map { userId -> UserId(userId) }.toImmutableList()
}.toImmutableMap(),
endTime = kind.endTime,
isEdited = kind.hasBeenEdited,
)
}
is TimelineItemContentKind.UnableToDecrypt -> {

View file

@ -16,32 +16,52 @@
package io.element.android.libraries.textcomposer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.libraries.designsystem.theme.bgSubtleTertiary
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.bgSubtleTertiary
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
import io.element.android.wysiwyg.compose.RichTextEditorStyle
internal object ElementRichTextEditorStyle {
object ElementRichTextEditorStyle {
@Composable
fun create(
fun composerStyle(
hasFocus: Boolean,
) : RichTextEditorStyle {
val baseStyle = common()
return baseStyle.copy(
text = baseStyle.text.copy(
color = if (hasFocus) {
ElementTheme.materialColors.primary
} else {
ElementTheme.materialColors.secondary
},
lineHeight = TextUnit.Unspecified,
includeFontPadding = true,
)
)
}
@Composable
fun textStyle(): RichTextEditorStyle {
return common()
}
@Composable
private fun common(): RichTextEditorStyle {
val colors = ElementTheme.colors
val m3colors = MaterialTheme.colorScheme
val codeCornerRadius = 4.dp
val codeBorderWidth = 1.dp
return RichTextEditorDefaults.style(
text = RichTextEditorDefaults.textStyle(
color = if (hasFocus) {
m3colors.primary
} else {
m3colors.secondary
},
lineHeight = 16.25.sp,
color = LocalTextStyle.current.color.takeIf { it.isSpecified } ?: LocalContentColor.current,
fontStyle = LocalTextStyle.current.fontStyle,
lineHeight = LocalTextStyle.current.lineHeight,
includeFontPadding = false,
),
cursor = RichTextEditorDefaults.cursorStyle(
color = colors.iconAccentTertiary,

View file

@ -441,9 +441,7 @@ private fun TextInput(
modifier = Modifier
.padding(top = 6.dp, bottom = 6.dp)
.fillMaxWidth(),
style = ElementRichTextEditorStyle.create(
hasFocus = state.hasFocus
),
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus),
resolveMentionDisplay = resolveMentionDisplay,
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
onError = onError

View file

@ -23,20 +23,59 @@ import android.text.style.ReplacementSpan
import kotlin.math.roundToInt
class MentionSpan(
val type: Type,
val backgroundColor: Int,
val textColor: Int,
) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return paint.measureText(text, start, end).roundToInt() + 40
val mentionText = getActualText(text, start)
var actualEnd = end
if (mentionText != text.toString()) {
actualEnd = end + 1
}
return paint.measureText(mentionText, start, actualEnd).roundToInt() + 40
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
val textSize = paint.measureText(text, start, end)
val rect = RectF(x, top.toFloat(), x + textSize + 40, bottom.toFloat())
val mentionText = getActualText(text, start)
var actualEnd = end
if (mentionText != text.toString()) {
actualEnd = end + 1
}
val textWidth = paint.measureText(mentionText, start, actualEnd)
// Extra vertical space to add below the baseline (y). This helps us center the span vertically
val extraVerticalSpace = y + paint.ascent() + paint.descent() - top
val rect = RectF(x, top.toFloat(), x + textWidth + 40, y.toFloat() + extraVerticalSpace)
paint.color = backgroundColor
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, paint)
paint.color = textColor
canvas.drawText(text!!, start, end, x + 20, y.toFloat(), paint)
canvas.drawText(mentionText, start, actualEnd, x + 20, y.toFloat(), paint)
}
private fun getActualText(text: CharSequence?, start: Int): String {
return when (type) {
Type.USER -> {
val mentionText = text.toString()
if (start in mentionText.indices && mentionText[start] != '@') {
mentionText.replaceRange(start, start, "@")
} else {
mentionText
}
}
Type.ROOM -> {
val mentionText = text.toString()
if (start in mentionText.indices && mentionText[start] != '#') {
mentionText.replaceRange(start, start, "#")
} else {
mentionText
}
}
}
}
enum class Type {
USER,
ROOM,
}
}

View file

@ -60,18 +60,21 @@ class MentionSpanProvider(
permalinkData is PermalinkData.UserLink -> {
val isCurrentUser = permalinkData.userId == currentSessionId.value
MentionSpan(
type = MentionSpan.Type.USER,
backgroundColor = if (isCurrentUser) currentUserBackgroundColor else otherBackgroundColor,
textColor = if (isCurrentUser) currentUserTextColor else otherTextColor,
)
}
text == "@room" && permalinkData is PermalinkData.FallbackLink -> {
MentionSpan(
type = MentionSpan.Type.USER,
backgroundColor = otherBackgroundColor,
textColor = otherTextColor,
)
}
else -> {
MentionSpan(
type = MentionSpan.Type.ROOM,
backgroundColor = otherBackgroundColor,
textColor = otherTextColor,
)
@ -97,17 +100,26 @@ internal fun MentionSpanPreview() {
provider.setup()
val textColor = ElementTheme.colors.textPrimary.toArgb()
val mentionSpan = provider.getMentionSpanFor("me", "https://matrix.to/#/@me:matrix.org")
val mentionSpan2 = provider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
fun mentionSpanMe() = provider.getMentionSpanFor("me", "https://matrix.to/#/@me:matrix.org")
fun mentionSpanOther() = provider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
fun mentionSpanRoom() = provider.getMentionSpanFor("room", "https://matrix.to/#/#room:matrix.org")
AndroidView(factory = { context ->
TextView(context).apply {
includeFontPadding = false
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
text = buildSpannedString {
append("This is a ")
append("@mention", mentionSpan, 0)
append("@mention", mentionSpanMe(), 0)
append(" to the current user and this is a ")
append("@mention", mentionSpan2, 0)
append(" to other user")
append("@mention", mentionSpanOther(), 0)
append(" to other user. This one is for a room: ")
append("#room:matrix.org", mentionSpanRoom(), 0)
append("\n\n")
append("This ")
append("mention", mentionSpanMe(), 0)
append(" didn't have an '@' and it was automatically added, same as this ")
append("room:matrix.org", mentionSpanRoom(), 0)
append(" one, which had no leading '#'.")
}
setTextColor(textColor)
}

View file

@ -46,14 +46,21 @@ class MentionSpanProviderTest {
@Test
fun `getting mention span for current user should return a MentionSpan with custom colors`() {
val mentionSpan = mentionSpanProvider.getMentionSpanFor("me", "https://matrix.to/#/${currentUserId.value}")
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@me:matrix.org", "https://matrix.to/#/${currentUserId.value}")
assertThat(mentionSpan.backgroundColor).isEqualTo(myUserColor)
assertThat(mentionSpan.textColor).isEqualTo(myUserColor)
}
@Test
fun `getting mention span for other user should return a MentionSpan with normal colors`() {
val mentionSpan = mentionSpanProvider.getMentionSpanFor("other", "https://matrix.to/#/@other:matrix.org")
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@other:matrix.org", "https://matrix.to/#/@other:matrix.org")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
}
@Test
fun `getting mention span for a room should return a MentionSpan with normal colors`() {
val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
}