Merge branch 'develop' into julioromano/poll_history_entry_point
This commit is contained in:
commit
5ac3f273ea
257 changed files with 916 additions and 1161 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue