Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/connect_sdk_to_global_notifications_ui

This commit is contained in:
David Langley 2023-09-12 16:30:36 +01:00
commit c3fbac4678
686 changed files with 7212 additions and 2257 deletions

View file

@ -67,6 +67,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate
import org.matrix.rustcomponents.sdk.NotificationProcessSetup
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
@ -100,9 +101,13 @@ class RustMatrixClient constructor(
client = client,
dispatchers = dispatchers,
)
private val notificationClient = client.notificationClient().use { builder ->
builder.filterByPushRules().finish()
}
private val notificationProcessSetup = NotificationProcessSetup.SingleProcess(syncService)
private val notificationClient = client.notificationClient(notificationProcessSetup)
.use { builder ->
builder
.filterByPushRules()
.finish()
}
private val notificationSettings = client.getNotificationSettings()
private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock)
@ -288,6 +293,7 @@ class RustMatrixClient constructor(
syncService.destroy()
innerRoomListService.destroy()
notificationClient.destroy()
notificationProcessSetup.destroy()
client.destroy()
}
@ -325,6 +331,7 @@ class RustMatrixClient constructor(
client.accountUrl()
}
}
override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
runCatching {
client.displayName()

View file

@ -53,7 +53,8 @@ class RustMatrixClientFactory @Inject constructor(
client.restoreSession(sessionData.toSession())
val syncService = client.syncService().finish()
val syncService = client.syncService()
.finish()
RustMatrixClient(
client = client,

View file

@ -66,7 +66,7 @@ import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import timber.log.Timber
import java.io.File
@ -227,31 +227,32 @@ class RustMatrixRoom(
}
}
override suspend fun sendMessage(message: String): Result<Unit> = withContext(roomDispatcher) {
override suspend fun sendMessage(body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId()
messageEventContentFromMarkdown(message).use { content ->
messageEventContentFromHtml(body, htmlBody).use { content ->
runCatching {
innerRoom.send(content, transactionId)
}
}
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result<Unit> = withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(messageEventContentFromMarkdown(message), originalEventId.value, transactionId?.value)
}
} else {
runCatching {
transactionId?.let { cancelSend(it) }
innerRoom.send(messageEventContentFromMarkdown(message), genTransactionId())
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> =
withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value)
}
} else {
runCatching {
transactionId?.let { cancelSend(it) }
innerRoom.send(messageEventContentFromHtml(body, htmlBody), genTransactionId())
}
}
}
}
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(roomDispatcher) {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.sendReply(messageEventContentFromMarkdown(message), eventId.value, genTransactionId())
innerRoom.sendReply(messageEventContentFromHtml(body, htmlBody), eventId.value, genTransactionId())
}
}

View file

@ -33,6 +33,8 @@ import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceStateListener
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener
import timber.log.Timber
fun RoomList.loadingStateFlow(): Flow<RoomListLoadingState> =
@ -83,6 +85,18 @@ fun RoomListService.stateFlow(): Flow<RoomListServiceState> =
}
}.buffer(Channel.UNLIMITED)
fun RoomListService.syncIndicator(): Flow<RoomListServiceSyncIndicator> =
mxCallbackFlow {
val listener = object : RoomListServiceSyncIndicatorListener {
override fun onUpdate(syncIndicator: RoomListServiceSyncIndicator) {
trySendBlocking(syncIndicator)
}
}
tryOrNull {
syncIndicator(listener)
}
}.buffer(Channel.UNLIMITED)
fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
return try {
room(roomId)

View file

@ -20,25 +20,26 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory()) {
suspend fun create(roomListItem: RoomListItem, room: Room?): RoomSummaryDetails {
val latestRoomMessage = roomListItem.latestEvent()?.use {
fun create(roomInfo: RoomInfo): RoomSummaryDetails {
val latestRoomMessage = roomInfo.latestEvent?.use {
roomMessageFactory.create(it)
}
return RoomSummaryDetails(
roomId = RoomId(roomListItem.id()),
name = roomListItem.name() ?: roomListItem.id(),
canonicalAlias = roomListItem.canonicalAlias(),
isDirect = roomListItem.isDirect(),
avatarURLString = roomListItem.avatarUrl(),
unreadNotificationCount = roomListItem.unreadNotifications().use { it.notificationCount().toInt() },
roomId = RoomId(roomInfo.id),
name = roomInfo.name ?: roomInfo.id,
canonicalAlias = roomInfo.canonicalAlias,
isDirect = roomInfo.isDirect,
avatarURLString = roomInfo.avatarUrl,
unreadNotificationCount = roomInfo.notificationCount.toInt(),
lastMessage = latestRoomMessage,
lastMessageTimestamp = latestRoomMessage?.originServerTs,
inviter = room?.inviter()?.let(RoomMemberMapper::map),
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
)
}
}

View file

@ -22,11 +22,10 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.util.UUID
@ -34,7 +33,6 @@ class RoomSummaryListProcessor(
private val roomSummaries: MutableStateFlow<List<RoomSummary>>,
private val roomListService: RoomListService,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
private val shouldFetchFullRoom: Boolean = false,
) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
@ -120,9 +118,9 @@ class RoomSummaryListProcessor(
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
roomListItem.fullRoomOrNull().use { fullRoom ->
roomListItem.roomInfo().use { roomInfo ->
RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(roomListItem, fullRoom)
details = roomSummaryDetailsFactory.create(roomInfo)
)
}
} ?: buildEmptyRoomSummary()
@ -130,14 +128,6 @@ class RoomSummaryListProcessor(
return builtRoomSummary
}
private fun RoomListItem.fullRoomOrNull(): Room? {
return if (shouldFetchFullRoom) {
fullRoom()
} else {
null
}
}
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) =
mutex.withLock {
val mutableRoomSummaries = roomSummaries.value.toMutableList()

View file

@ -38,6 +38,7 @@ import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator
import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
@ -52,9 +53,9 @@ class RustRoomListService(
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory)
private val invitesLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory)
init {
sessionCoroutineScope.launch(dispatcher) {
@ -106,6 +107,15 @@ class RustRoomListService(
}
}
override val syncIndicator: StateFlow<RoomListService.SyncIndicator> =
innerRoomListService.syncIndicator()
.map { it.toSyncIndicator() }
.onEach { syncIndicator ->
Timber.d("SyncIndicator = $syncIndicator")
}
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.SyncIndicator.Hide)
override val state: StateFlow<RoomListService.State> =
innerRoomListService.stateFlow()
.map { it.toRoomListState() }
@ -126,6 +136,7 @@ private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
private fun RoomListServiceState.toRoomListState(): RoomListService.State {
return when (this) {
RoomListServiceState.INITIAL,
RoomListServiceState.RECOVERING,
RoomListServiceState.SETTING_UP -> RoomListService.State.Idle
RoomListServiceState.RUNNING -> RoomListService.State.Running
RoomListServiceState.ERROR -> RoomListService.State.Error
@ -133,6 +144,13 @@ private fun RoomListServiceState.toRoomListState(): RoomListService.State {
}
}
private fun RoomListServiceSyncIndicator.toSyncIndicator(): RoomListService.SyncIndicator {
return when (this) {
RoomListServiceSyncIndicator.SHOW -> RoomListService.SyncIndicator.Show
RoomListServiceSyncIndicator.HIDE -> RoomListService.SyncIndicator.Hide
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow<List<RoomListEntriesUpdate>> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)

View file

@ -34,85 +34,92 @@ import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.TimelineItemContentKind
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.EncryptedMessage as RustEncryptedMessage
import org.matrix.rustcomponents.sdk.MembershipChange as RustMembershipChange
import org.matrix.rustcomponents.sdk.OtherState as RustOtherState
class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMapper = EventMessageMapper()) {
fun map(content: TimelineItemContent): EventContent = content.use {
when (val kind = it.kind()) {
is TimelineItemContentKind.FailedToParseMessageLike -> {
FailedToParseMessageLikeContent(
eventType = kind.eventType,
error = kind.error
)
fun map(content: TimelineItemContent): EventContent {
return content.use {
content.kind().use { kind ->
map(content, kind)
}
is TimelineItemContentKind.FailedToParseState -> {
FailedToParseStateContent(
eventType = kind.eventType,
stateKey = kind.stateKey,
error = kind.error
)
}
TimelineItemContentKind.Message -> {
val message = it.asMessage()
if (message == null) {
UnknownContent
} else {
eventMessageMapper.map(message)
}
}
is TimelineItemContentKind.ProfileChange -> {
ProfileChangeContent(
displayName = kind.displayName,
prevDisplayName = kind.prevDisplayName,
avatarUrl = kind.avatarUrl,
prevAvatarUrl = kind.prevAvatarUrl
)
}
TimelineItemContentKind.RedactedMessage -> {
RedactedContent
}
is TimelineItemContentKind.RoomMembership -> {
RoomMembershipContent(
UserId(kind.userId),
kind.change?.map()
)
}
is TimelineItemContentKind.State -> {
StateContent(
stateKey = kind.stateKey,
content = kind.content.map()
)
}
is TimelineItemContentKind.Sticker -> {
StickerContent(
body = kind.body,
info = kind.info.map(),
url = kind.url,
)
}
is TimelineItemContentKind.Poll -> {
PollContent(
question = kind.question,
kind = kind.kind.map(),
maxSelections = kind.maxSelections,
answers = kind.answers.map { answer -> answer.map() },
votes = kind.votes.mapValues { vote ->
vote.value.map { userId -> UserId(userId) }
},
endTime = kind.endTime,
)
}
is TimelineItemContentKind.UnableToDecrypt -> {
UnableToDecryptContent(
data = kind.msg.map()
)
}
else -> UnknownContent
}
}
private fun map(content: TimelineItemContent, kind: TimelineItemContentKind) = when (kind) {
is TimelineItemContentKind.FailedToParseMessageLike -> {
FailedToParseMessageLikeContent(
eventType = kind.eventType,
error = kind.error
)
}
is TimelineItemContentKind.FailedToParseState -> {
FailedToParseStateContent(
eventType = kind.eventType,
stateKey = kind.stateKey,
error = kind.error
)
}
TimelineItemContentKind.Message -> {
val message = content.asMessage()
if (message == null) {
UnknownContent
} else {
eventMessageMapper.map(message)
}
}
is TimelineItemContentKind.ProfileChange -> {
ProfileChangeContent(
displayName = kind.displayName,
prevDisplayName = kind.prevDisplayName,
avatarUrl = kind.avatarUrl,
prevAvatarUrl = kind.prevAvatarUrl
)
}
TimelineItemContentKind.RedactedMessage -> {
RedactedContent
}
is TimelineItemContentKind.RoomMembership -> {
RoomMembershipContent(
UserId(kind.userId),
kind.change?.map()
)
}
is TimelineItemContentKind.State -> {
StateContent(
stateKey = kind.stateKey,
content = kind.content.map()
)
}
is TimelineItemContentKind.Sticker -> {
StickerContent(
body = kind.body,
info = kind.info.map(),
url = kind.url,
)
}
is TimelineItemContentKind.Poll -> {
PollContent(
question = kind.question,
kind = kind.kind.map(),
maxSelections = kind.maxSelections,
answers = kind.answers.map { answer -> answer.map() },
votes = kind.votes.mapValues { vote ->
vote.value.map { userId -> UserId(userId) }
},
endTime = kind.endTime,
)
}
is TimelineItemContentKind.UnableToDecrypt -> {
UnableToDecryptContent(
data = kind.msg.map()
)
}
else -> UnknownContent
}
}
private fun RustMembershipChange.map(): MembershipChange {