Merge branch 'develop' into julioromano/poll_history_entry_point

This commit is contained in:
Benoit Marty 2023-12-14 16:58:23 +01:00 committed by GitHub
commit 6bf416045d
24 changed files with 219 additions and 54 deletions

View file

@ -9,13 +9,10 @@ appId: ${APP_ID}
- tapOn: "Other" - tapOn: "Other"
- tapOn: - tapOn:
id: "change_server-server" id: "change_server-server"
# Test server that does not support sliding sync. - inputText: "element"
- inputText: "https://kieranml.ems-support.element.dev"
- hideKeyboard - hideKeyboard
- tapOn: "kieranml.ems-support.element.dev" - tapOn: "element.io"
- extendedWaitUntil: # Revert to matrix.org
visible: "This server currently doesnt support sliding sync." - tapOn:
timeout: 10000 id: "login-change_server"
- tapOn: "Cancel" - tapOn: "matrix.org"
- back
- back

View file

@ -67,6 +67,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
@ -149,6 +150,23 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
) )
observeSyncStateAndNetworkStatus() observeSyncStateAndNetworkStatus()
observeInvitesLoadingState()
}
private fun observeInvitesLoadingState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
matrixClient.roomListService.invites.loadingState
.collect { inviteState ->
when (inviteState) {
is RoomList.LoadingState.Loaded -> if (inviteState.numberOfRooms == 0) {
backstack.removeLast(NavTarget.InviteList)
}
RoomList.LoadingState.NotLoaded -> Unit
}
}
}
}
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)

1
changelog.d/1449.bugfix Normal file
View file

@ -0,0 +1 @@
Adjust mention pills font weight and horizontal padding

1
changelog.d/1918.misc Normal file
View file

@ -0,0 +1 @@
Add ability to see the room avatar in the media viewer.

1
changelog.d/2022.feature Normal file
View file

@ -0,0 +1 @@
Always close the invite list screen when there is no more invite.

View file

@ -242,7 +242,6 @@ class MessagesFlowNode @AssistedInject constructor(
backstack.push(navTarget) backstack.push(navTarget)
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
val mediaSource = event.content.videoSource
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, name = event.content.body,
@ -250,13 +249,12 @@ class MessagesFlowNode @AssistedInject constructor(
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
), ),
mediaSource = mediaSource, mediaSource = event.content.videoSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
backstack.push(navTarget) backstack.push(navTarget)
} }
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
val mediaSource = event.content.fileSource
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, name = event.content.body,
@ -264,13 +262,12 @@ class MessagesFlowNode @AssistedInject constructor(
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
), ),
mediaSource = mediaSource, mediaSource = event.content.fileSource,
thumbnailSource = event.content.thumbnailSource, thumbnailSource = event.content.thumbnailSource,
) )
backstack.push(navTarget) backstack.push(navTarget)
} }
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
val mediaSource = event.content.mediaSource
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, name = event.content.body,
@ -278,7 +275,7 @@ class MessagesFlowNode @AssistedInject constructor(
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
), ),
mediaSource = mediaSource, mediaSource = event.content.mediaSource,
thumbnailSource = null, thumbnailSource = null,
) )
backstack.push(navTarget) backstack.push(navTarget)

View file

@ -48,6 +48,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@ -233,7 +234,7 @@ class TimelinePresenter @AssistedInject constructor(
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
lastReadReceiptIndex.value = firstVisibleIndex lastReadReceiptIndex.value = firstVisibleIndex
lastReadReceiptId.value = eventId lastReadReceiptId.value = eventId
timeline.sendReadReceipt(eventId) timeline.sendReadReceipt(eventId = eventId, receiptType = ReceiptType.READ)
} }
} }

View file

@ -35,7 +35,7 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
import io.element.android.features.roomdetails.impl.members.details.avatar.RoomMemberAvatarPreviewNode import io.element.android.features.roomdetails.impl.members.details.avatar.AvatarPreviewNode
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@ -89,7 +89,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
@Parcelize @Parcelize
data class MemberAvatarPreview(val userName: String, val avatarUrl: String) : NavTarget data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
@Parcelize @Parcelize
data object PollHistory : NavTarget data object PollHistory : NavTarget
@ -115,8 +115,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false)) backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false))
} }
override fun openAvatarPreview(username: String, url: String) { override fun openAvatarPreview(name: String, url: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, url)) backstack.push(NavTarget.AvatarPreview(name, url))
} }
override fun openPollHistory() { override fun openPollHistory() {
@ -160,7 +160,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
is NavTarget.RoomMemberDetails -> { is NavTarget.RoomMemberDetails -> {
val callback = object : RoomMemberDetailsNode.Callback { val callback = object : RoomMemberDetailsNode.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) { override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, avatarUrl)) backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
} }
override fun onStartDM(roomId: RoomId) { override fun onStartDM(roomId: RoomId) {
@ -170,22 +170,22 @@ class RoomDetailsFlowNode @AssistedInject constructor(
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
createNode<RoomMemberDetailsNode>(buildContext, plugins) createNode<RoomMemberDetailsNode>(buildContext, plugins)
} }
is NavTarget.MemberAvatarPreview -> { is NavTarget.AvatarPreview -> {
// We need to fake the MimeType here for the viewer to work. // We need to fake the MimeType here for the viewer to work.
val mimeType = MimeTypes.Images val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs( val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = navTarget.userName, name = navTarget.name,
mimeType = mimeType, mimeType = mimeType,
formattedFileSize = "", formattedFileSize = "",
fileExtension = "" fileExtension = ""
), ),
mediaSource = MediaSource(url = navTarget.avatarUrl), mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = MediaSource(url = navTarget.avatarUrl), thumbnailSource = null,
canDownload = false, canDownload = false,
canShare = false, canShare = false,
) )
createNode<RoomMemberAvatarPreviewNode>(buildContext, listOf(input)) createNode<AvatarPreviewNode>(buildContext, listOf(input))
} }
is NavTarget.PollHistory -> { is NavTarget.PollHistory -> {

View file

@ -52,7 +52,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openInviteMembers() fun openInviteMembers()
fun editRoomDetails() fun editRoomDetails()
fun openRoomNotificationSettings() fun openRoomNotificationSettings()
fun openAvatarPreview(username: String, url: String) fun openAvatarPreview(name: String, url: String)
fun openPollHistory() fun openPollHistory()
} }
@ -116,8 +116,8 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.editRoomDetails() } callbacks.forEach { it.editRoomDetails() }
} }
private fun openAvatarPreview(username: String, url: String) { private fun openAvatarPreview(name: String, url: String) {
callbacks.forEach { it.openAvatarPreview(username, url) } callbacks.forEach { it.openAvatarPreview(name, url) }
} }
@Composable @Composable

View file

@ -16,6 +16,7 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -77,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.room.RoomMember 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.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -89,7 +91,7 @@ fun RoomDetailsView(
openRoomMemberList: () -> Unit, openRoomMemberList: () -> Unit,
openRoomNotificationSettings: () -> Unit, openRoomNotificationSettings: () -> Unit,
invitePeople: () -> Unit, invitePeople: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit, openAvatarPreview: (name: String, url: String) -> Unit,
openPollHistory: () -> Unit, openPollHistory: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -121,7 +123,10 @@ fun RoomDetailsView(
avatarUrl = state.roomAvatarUrl, avatarUrl = state.roomAvatarUrl,
roomId = state.roomId, roomId = state.roomId,
roomName = state.roomName, roomName = state.roomName,
roomAlias = state.roomAlias roomAlias = state.roomAlias,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.roomName, avatarUrl)
},
) )
MainActionsSection( MainActionsSection(
state = state, state = state,
@ -135,10 +140,8 @@ fun RoomDetailsView(
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl, avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
userId = member.userId.value, userId = member.userId.value,
userName = state.roomName, userName = state.roomName,
openAvatarPreview = { openAvatarPreview = { avatarUrl ->
if (member.avatarUrl != null) { openAvatarPreview(member.getBestName(), avatarUrl)
openAvatarPreview(member.displayName ?: member.userId.value, member.avatarUrl!!)
}
}, },
) )
RoomMemberMainActionsSection(onShareUser = ::onShareMember) RoomMemberMainActionsSection(onShareUser = ::onShareMember)
@ -270,6 +273,7 @@ private fun RoomHeaderSection(
roomId: String, roomId: String,
roomName: String, roomName: String,
roomAlias: String?, roomAlias: String?,
openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( Column(
@ -280,7 +284,9 @@ private fun RoomHeaderSection(
) { ) {
Avatar( Avatar(
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader), avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader),
modifier = Modifier.size(70.dp) modifier = Modifier
.size(70.dp)
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class RoomMemberAvatarPreviewNode @AssistedInject constructor( class AvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory, presenterFactory: MediaViewerPresenter.Factory,

View file

@ -16,7 +16,7 @@ datastore = "1.0.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
constraintlayout_compose = "1.0.1" constraintlayout_compose = "1.0.1"
lifecycle = "2.7.0-rc02" lifecycle = "2.7.0-rc02"
activity = "1.8.1" activity = "1.8.2"
media3 = "1.2.0" media3 = "1.2.0"
# Compose # Compose
@ -91,7 +91,7 @@ androidx_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.9.0" androidx_webkit = "androidx.webkit:webkit:1.9.0"
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha12" androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-beta01"
androidx_compose_ui = { module = "androidx.compose.ui:ui" } androidx_compose_ui = { module = "androidx.compose.ui:ui" }
androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" } androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" }
@ -150,7 +150,7 @@ jsoup = "org.jsoup:jsoup:1.17.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.1" molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.1"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.76" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.77"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -0,0 +1,42 @@
/*
* 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.designsystem.text
import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
@Composable
fun TextStyle.rememberTypeface(): State<Typeface> {
val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current
@Suppress("UNCHECKED_CAST")
return remember(resolver, this) {
resolver.resolve(
fontFamily = fontFamily,
fontWeight = fontWeight ?: FontWeight.Normal,
fontStyle = fontStyle ?: FontStyle.Normal,
fontSynthesis = fontSynthesis ?: FontSynthesis.All,
)
} as State<Typeface>
}

View file

@ -32,3 +32,7 @@ data class RoomMember(
enum class RoomMembershipState { enum class RoomMembershipState {
BAN, INVITE, JOIN, KNOCK, LEAVE BAN, INVITE, JOIN, KNOCK, LEAVE
} }
fun RoomMember.getBestName(): String {
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
}

View file

@ -44,5 +44,5 @@ interface MatrixTimeline : AutoCloseable {
suspend fun paginateBackwards(requestSize: Int): Result<Unit> suspend fun paginateBackwards(requestSize: Int): Result<Unit>
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
suspend fun sendReadReceipt(eventId: EventId): Result<Unit> suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
} }

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.timeline
enum class ReceiptType {
READ,
READ_PRIVATE,
FULLY_READ;
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import org.matrix.rustcomponents.sdk.ReceiptType as RustReceiptType
internal fun ReceiptType.toRustReceiptType(): RustReceiptType = when (this) {
ReceiptType.READ -> RustReceiptType.READ
ReceiptType.READ_PRIVATE -> RustReceiptType.READ_PRIVATE
ReceiptType.FULLY_READ -> RustReceiptType.FULLY_READ
}

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.TimelineException
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
@ -230,9 +231,15 @@ class RustMatrixTimeline(
return isInit.get() && paginationState.value.canBackPaginate return isInit.get() && paginationState.value.canBackPaginate
} }
override suspend fun sendReadReceipt(eventId: EventId) = withContext(dispatcher) { override suspend fun sendReadReceipt(
eventId: EventId,
receiptType: ReceiptType,
) = withContext(dispatcher) {
runCatching { runCatching {
innerTimeline.sendReadReceipt(eventId = eventId.value) innerTimeline.sendReadReceipt(
receiptType = receiptType.toRustReceiptType(),
eventId = eventId.value,
)
} }
} }

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -77,7 +78,10 @@ class FakeMatrixTimeline(
Result.success(Unit) Result.success(Unit)
} }
override suspend fun sendReadReceipt(eventId: EventId): Result<Unit> = simulateLongTask { override suspend fun sendReadReceipt(
eventId: EventId,
receiptType: ReceiptType,
): Result<Unit> = simulateLongTask {
sendReadReceiptCount++ sendReadReceiptCount++
sendReadReceiptLatch?.complete(Unit) sendReadReceiptLatch?.complete(Unit)
Result.success(Unit) Result.success(Unit)

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.textcomposer.mentions
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Typeface
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -26,6 +27,9 @@ class MentionSpan(
val type: Type, val type: Type,
val backgroundColor: Int, val backgroundColor: Int,
val textColor: Int, val textColor: Int,
val startPadding: Int,
val endPadding: Int,
val typeface: Typeface = Typeface.DEFAULT,
) : ReplacementSpan() { ) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
@ -34,7 +38,8 @@ class MentionSpan(
if (mentionText != text.toString()) { if (mentionText != text.toString()) {
actualEnd = end + 1 actualEnd = end + 1
} }
return paint.measureText(mentionText, start, actualEnd).roundToInt() + 40 paint.typeface = typeface
return paint.measureText(mentionText, start, actualEnd).roundToInt() + startPadding + endPadding
} }
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
@ -46,11 +51,12 @@ class MentionSpan(
val textWidth = paint.measureText(mentionText, start, actualEnd) val textWidth = paint.measureText(mentionText, start, actualEnd)
// Extra vertical space to add below the baseline (y). This helps us center the span vertically // 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 extraVerticalSpace = y + paint.ascent() + paint.descent() - top
val rect = RectF(x, top.toFloat(), x + textWidth + 40, y.toFloat() + extraVerticalSpace) val rect = RectF(x, top.toFloat(), x + textWidth + startPadding + endPadding, y.toFloat() + extraVerticalSpace)
paint.color = backgroundColor paint.color = backgroundColor
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, paint) canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, paint)
paint.color = textColor paint.color = textColor
canvas.drawText(mentionText, start, actualEnd, x + 20, y.toFloat(), paint) paint.typeface = typeface
canvas.drawText(mentionText, start, actualEnd, x + startPadding, y.toFloat(), paint)
} }
private fun getActualText(text: CharSequence?, start: Int): String { private fun getActualText(text: CharSequence?, start: Int): String {

View file

@ -17,16 +17,24 @@
package io.element.android.libraries.textcomposer.mentions package io.element.android.libraries.textcomposer.mentions
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.rememberTypeface
import io.element.android.libraries.designsystem.theme.currentUserMentionPillBackground import io.element.android.libraries.designsystem.theme.currentUserMentionPillBackground
import io.element.android.libraries.designsystem.theme.currentUserMentionPillText import io.element.android.libraries.designsystem.theme.currentUserMentionPillText
import io.element.android.libraries.designsystem.theme.mentionPillBackground import io.element.android.libraries.designsystem.theme.mentionPillBackground
@ -34,7 +42,6 @@ import io.element.android.libraries.designsystem.theme.mentionPillText
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.compound.theme.ElementTheme
@Stable @Stable
class MentionSpanProvider( class MentionSpanProvider(
@ -44,6 +51,10 @@ class MentionSpanProvider(
private var otherTextColor: Int = 0, private var otherTextColor: Int = 0,
private var otherBackgroundColor: Int = Color.WHITE, private var otherBackgroundColor: Int = Color.WHITE,
) { ) {
private val paddingValues = PaddingValues(start = 4.dp, end = 6.dp)
private val paddingValuesPx = mutableStateOf(0 to 0)
private val typeface = mutableStateOf(Typeface.DEFAULT)
@Suppress("ComposableNaming") @Suppress("ComposableNaming")
@Composable @Composable
@ -52,10 +63,18 @@ class MentionSpanProvider(
currentUserBackgroundColor = ElementTheme.colors.currentUserMentionPillBackground.toArgb() currentUserBackgroundColor = ElementTheme.colors.currentUserMentionPillBackground.toArgb()
otherTextColor = ElementTheme.colors.mentionPillText.toArgb() otherTextColor = ElementTheme.colors.mentionPillText.toArgb()
otherBackgroundColor = ElementTheme.colors.mentionPillBackground.toArgb() otherBackgroundColor = ElementTheme.colors.mentionPillBackground.toArgb()
typeface.value = ElementTheme.typography.fontBodyLgMedium.rememberTypeface().value
with(LocalDensity.current) {
val leftPadding = paddingValues.calculateLeftPadding(LocalLayoutDirection.current).roundToPx()
val rightPadding = paddingValues.calculateRightPadding(LocalLayoutDirection.current).roundToPx()
paddingValuesPx.value = leftPadding to rightPadding
}
} }
fun getMentionSpanFor(text: String, url: String): MentionSpan { fun getMentionSpanFor(text: String, url: String): MentionSpan {
val permalinkData = PermalinkParser.parse(url) val permalinkData = PermalinkParser.parse(url)
val (startPaddingPx, endPaddingPx) = paddingValuesPx.value
return when { return when {
permalinkData is PermalinkData.UserLink -> { permalinkData is PermalinkData.UserLink -> {
val isCurrentUser = permalinkData.userId == currentSessionId.value val isCurrentUser = permalinkData.userId == currentSessionId.value
@ -63,6 +82,9 @@ class MentionSpanProvider(
type = MentionSpan.Type.USER, type = MentionSpan.Type.USER,
backgroundColor = if (isCurrentUser) currentUserBackgroundColor else otherBackgroundColor, backgroundColor = if (isCurrentUser) currentUserBackgroundColor else otherBackgroundColor,
textColor = if (isCurrentUser) currentUserTextColor else otherTextColor, textColor = if (isCurrentUser) currentUserTextColor else otherTextColor,
startPadding = startPaddingPx,
endPadding = endPaddingPx,
typeface = typeface.value,
) )
} }
text == "@room" && permalinkData is PermalinkData.FallbackLink -> { text == "@room" && permalinkData is PermalinkData.FallbackLink -> {
@ -70,6 +92,9 @@ class MentionSpanProvider(
type = MentionSpan.Type.USER, type = MentionSpan.Type.USER,
backgroundColor = otherBackgroundColor, backgroundColor = otherBackgroundColor,
textColor = otherTextColor, textColor = otherTextColor,
startPadding = startPaddingPx,
endPadding = endPaddingPx,
typeface = typeface.value,
) )
} }
else -> { else -> {
@ -77,6 +102,9 @@ class MentionSpanProvider(
type = MentionSpan.Type.ROOM, type = MentionSpan.Type.ROOM,
backgroundColor = otherBackgroundColor, backgroundColor = otherBackgroundColor,
textColor = otherTextColor, textColor = otherTextColor,
startPadding = startPaddingPx,
endPadding = endPaddingPx,
typeface = typeface.value,
) )
} }
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8819c4ceeb21f96ba6d584ee78e4aca1a9cf2c87412f70e8f00eff09de61abc8 oid sha256:e541cfb32e9d3095d6499c4c25e4bb4158c10886d59accf6006290bda344e8b2
size 43788 size 43927

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ffbb340bd78c7806dd52be9454057b5ed4a3f9831f9ecb1c99b10c0759ba7f85 oid sha256:06cd8d611bf8149d3c86b8d9a4aa5613dc9f32f89156081472f98d6410e98c4d
size 37091 size 37107

View file

@ -8,6 +8,7 @@ read -p "Do you want to build the Rust SDK from local source (yes/no) default to
buildLocal=${buildLocal:-yes} buildLocal=${buildLocal:-yes}
date=$(gdate +%Y%m%d%H%M%S) date=$(gdate +%Y%m%d%H%M%S)
elementPwd=`pwd`
# Ask for the Rust SDK local source path # Ask for the Rust SDK local source path
# if folder rustSdk/ exists, use it as default # if folder rustSdk/ exists, use it as default
@ -28,7 +29,7 @@ else
cd matrix-rust-sdk-$date cd matrix-rust-sdk-$date
git checkout ${rustSdkBranch} git checkout ${rustSdkBranch}
rustSdkPath=$(pwd) rustSdkPath=$(pwd)
cd ../element-x-android cd ${elementPwd}
fi fi
@ -46,6 +47,8 @@ fi
read -p "Do you want to build the app after (yes/no) default to yes? " buildApp read -p "Do you want to build the app after (yes/no) default to yes? " buildApp
buildApp=${buildApp:-yes} buildApp=${buildApp:-yes}
cd ${elementPwd}
# If folder ../matrix-rust-components-kotlin does not exist, clone the repo # If folder ../matrix-rust-components-kotlin does not exist, clone the repo
if [ ! -d "../matrix-rust-components-kotlin" ]; then if [ ! -d "../matrix-rust-components-kotlin" ]; then
printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n" printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n"
@ -59,9 +62,9 @@ git checkout main
git pull git pull
printf "\nBuilding the SDK for aarch64-linux-android...\n\n" printf "\nBuilding the SDK for aarch64-linux-android...\n\n"
./scripts/build.sh -p ${rustSdkPath} -m sdk -t aarch64-linux-android -o ../element-x-android/libraries/rustsdk ./scripts/build.sh -p ${rustSdkPath} -m sdk -t aarch64-linux-android -o ${elementPwd}/libraries/rustsdk
cd ../element-x-android cd ${elementPwd}
mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar
mkdir -p ./libraries/rustsdk/sdks mkdir -p ./libraries/rustsdk/sdks
cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/sdks/matrix-rust-sdk-${date}.aar cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/sdks/matrix-rust-sdk-${date}.aar