Update SDK version to 25.03.13 and fix breaking changes (#4406)
Breaking changes addressed: * Make `MatrixClient.getNotificationSettings()` async, cache its result. * Use `RoomInfo` for accessing the updated room's info. * Refactor `MatrixRoom` so it always receives an initial `MatrixRoomInfo` value: this value will be used to make `MatrixRoom.roomInfoFlow` a `StateFlow` so we can assume the initial updated Room data will be present. * Fetch encryption state when loading a room if it's unknown
This commit is contained in:
parent
0c07a8165f
commit
fccd881b1f
76 changed files with 647 additions and 431 deletions
|
|
@ -59,11 +59,14 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
|
|||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MessagesNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
|
||||
|
|
@ -108,7 +111,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
super.onBuilt()
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
analyticsService.capture(room.toAnalyticsViewRoom())
|
||||
coroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
|
||||
},
|
||||
onDestroy = {
|
||||
mediaPlayer.close()
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
override fun present(): MessagesState {
|
||||
htmlConverterProvider.Update(currentUserId = room.sessionId)
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(null)
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val composerState = composerPresenter.present()
|
||||
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
|
||||
|
|
@ -147,13 +147,13 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
||||
|
||||
val roomName: AsyncData<String> by remember {
|
||||
derivedStateOf { roomInfo?.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
}
|
||||
val roomAvatar: AsyncData<AvatarData> by remember {
|
||||
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
derivedStateOf { AsyncData.Success(roomInfo.avatarData()) }
|
||||
}
|
||||
val heroes by remember {
|
||||
derivedStateOf { roomInfo?.heroes().orEmpty().toPersistentList() }
|
||||
derivedStateOf { roomInfo.heroes().toPersistentList() }
|
||||
}
|
||||
|
||||
var hasDismissedInviteDialog by rememberSaveable {
|
||||
|
|
@ -164,14 +164,20 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
// as those will be handled by the timeline.
|
||||
withContext(dispatchers.io) {
|
||||
room.setUnreadFlag(isUnread = false)
|
||||
|
||||
// If for some reason the encryption state is unknown, fetch it
|
||||
if (roomInfo.isEncrypted == null) {
|
||||
room.getUpdatedIsEncrypted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val inviteProgress = remember { mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) }
|
||||
var showReinvitePrompt by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerState.textEditorState.hasFocus(), syncUpdateFlow.value) {
|
||||
val composerHasFocus by remember { derivedStateOf { composerState.textEditorState.hasFocus() } }
|
||||
LaunchedEffect(hasDismissedInviteDialog, composerHasFocus, roomInfo) {
|
||||
withContext(dispatchers.io) {
|
||||
showReinvitePrompt = !hasDismissedInviteDialog && composerState.textEditorState.hasFocus() && room.isDm && room.activeMemberCount == 1L
|
||||
showReinvitePrompt = !hasDismissedInviteDialog && composerHasFocus && roomInfo.isDm && roomInfo.activeMembersCount == 1L
|
||||
}
|
||||
}
|
||||
val isOnline by syncService.isOnline().collectAsState()
|
||||
|
|
@ -189,9 +195,8 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
val dmRoomMember by room.getDirectRoomMember(membersState)
|
||||
val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges
|
||||
|
||||
// TODO use `RoomInfo.isEncrypted` as a key here once it's available
|
||||
LifecycleResumeEffect(dmRoomMember, roomMemberIdentityStateChanges) {
|
||||
if (room.isEncrypted) {
|
||||
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
|
||||
if (roomInfo.isEncrypted == true) {
|
||||
val dmRoomMemberId = dmRoomMember?.userId
|
||||
localCoroutineScope.launch {
|
||||
dmRoomMemberId?.let { userId ->
|
||||
|
|
@ -275,7 +280,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
return AvatarData(
|
||||
id = id.value,
|
||||
name = name,
|
||||
url = avatarUrl ?: room.avatarUrl,
|
||||
url = avatarUrl ?: room.info().avatarUrl,
|
||||
size = AvatarSize.TimelineRoom
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ class MessageComposerPresenter @AssistedInject constructor(
|
|||
|
||||
suspend fun canSendRoomMention(): Boolean {
|
||||
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
|
||||
return !room.isDm && userCanSendAtRoom
|
||||
return !room.isDm() && userCanSendAtRoom
|
||||
}
|
||||
|
||||
// This will trigger a search immediately when `@` is typed
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ import io.element.android.libraries.architecture.Presenter
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canPinUnpin
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.ui.room.isDmAsState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
|
|
@ -85,10 +85,12 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesListState {
|
||||
val timelineRoomInfo = remember {
|
||||
val isDm by room.isDmAsState()
|
||||
|
||||
val timelineRoomInfo = remember(isDm) {
|
||||
TimelineRoomInfo(
|
||||
isDm = room.isDm,
|
||||
name = room.displayName,
|
||||
isDm = isDm,
|
||||
name = room.info().name,
|
||||
// We don't need to compute those values
|
||||
userHasPermissionToSendMessage = false,
|
||||
userHasPermissionToSendReaction = false,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.features.poll.api.actions.EndPollAction
|
|||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
|
|
@ -95,7 +96,7 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
|
||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
|
||||
|
|
@ -231,15 +232,15 @@ class TimelinePresenter @AssistedInject constructor(
|
|||
|
||||
val typingNotificationState = typingNotificationPresenter.present()
|
||||
val roomCallState = roomCallStatePresenter.present()
|
||||
val timelineRoomInfo by remember(typingNotificationState, roomCallState) {
|
||||
val timelineRoomInfo by remember(typingNotificationState, roomCallState, roomInfo) {
|
||||
derivedStateOf {
|
||||
TimelineRoomInfo(
|
||||
name = room.displayName,
|
||||
isDm = room.isDm,
|
||||
name = roomInfo.name,
|
||||
isDm = roomInfo.isDm.orFalse(),
|
||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||
roomCallState = roomCallState,
|
||||
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
|
||||
pinnedEventIds = roomInfo.pinnedEventIds.orEmpty(),
|
||||
typingNotificationState = typingNotificationState,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -536,15 +536,15 @@ class MessagesPresenterTest {
|
|||
fun `present - shows prompt to reinvite users in DM`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
isDirect = true,
|
||||
activeMemberCount = 1L,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOtherResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
).apply {
|
||||
givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1))
|
||||
}
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -552,7 +552,8 @@ class MessagesPresenterTest {
|
|||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
// When the input field is focused we show the alert
|
||||
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
|
||||
skipItems(1)
|
||||
// Skip intermediate states
|
||||
skipItems(2)
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isTrue()
|
||||
// If it's dismissed then we stop showing the alert
|
||||
|
|
@ -567,20 +568,22 @@ class MessagesPresenterTest {
|
|||
fun `present - doesn't show reinvite prompt in non-direct room`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
isDirect = false,
|
||||
activeMemberCount = 1L,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOtherResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
).apply {
|
||||
givenRoomInfo(aRoomInfo(isDirect = false, joinedMembersCount = 1, activeMembersCount = 1))
|
||||
}
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
|
||||
// Skip intermediate events
|
||||
skipItems(1)
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -590,20 +593,22 @@ class MessagesPresenterTest {
|
|||
fun `present - doesn't show reinvite prompt if other party is present`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
isDirect = true,
|
||||
activeMemberCount = 2L,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOtherResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
)
|
||||
).apply {
|
||||
givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 2, activeMembersCount = 2))
|
||||
}
|
||||
val presenter = createMessagesPresenter(matrixRoom = room)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showReinvitePrompt).isFalse()
|
||||
(initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
|
||||
// Skip intermediate events
|
||||
skipItems(1)
|
||||
val focusedState = awaitItem()
|
||||
assertThat(focusedState.showReinvitePrompt).isFalse()
|
||||
}
|
||||
|
|
@ -1090,28 +1095,25 @@ class MessagesPresenterTest {
|
|||
fun `present - when room is encrypted and a DM, the DM user's identity state is fetched onResume`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
sessionId = A_SESSION_ID,
|
||||
isEncrypted = true,
|
||||
isDirect = true,
|
||||
activeMemberCount = 2L,
|
||||
canUserSendMessageResult = { _, _ -> Result.success(true) },
|
||||
canRedactOwnResult = { Result.success(true) },
|
||||
canRedactOtherResult = { Result.success(true) },
|
||||
canUserJoinCallResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) },
|
||||
canUserPinUnpinResult = { Result.success(true) },
|
||||
initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true)
|
||||
).apply {
|
||||
givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2))))
|
||||
givenRoomInfo(aRoomInfo(id = roomId, name = "", isDirect = true))
|
||||
}
|
||||
val encryptionService = FakeEncryptionService(getUserIdentityResult = { Result.success(IdentityState.Verified) })
|
||||
|
||||
val presenter = createMessagesPresenter(matrixRoom = room, encryptionService = encryptionService)
|
||||
val lifecycleOwner = FakeLifecycleOwner()
|
||||
presenter.testWithLifecycleOwner(lifecycleOwner) {
|
||||
skipItems(1)
|
||||
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.dmUserVerificationState).isNull()
|
||||
|
||||
skipItems(1)
|
||||
ensureAllEventsConsumed()
|
||||
|
||||
lifecycleOwner.givenState(Lifecycle.State.RESUMED)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
|||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
|
|
@ -44,7 +45,9 @@ class IdentityChangeStatePresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - when the room emits identity change, the presenter emits new state`() = runTest {
|
||||
val room = FakeMatrixRoom(isEncrypted = true)
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||
}
|
||||
val presenter = createIdentityChangeStatePresenter(room)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -67,10 +70,7 @@ class IdentityChangeStatePresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - when the clear room emits identity change, the presenter does not emit new state`() = runTest {
|
||||
val room = FakeMatrixRoom(
|
||||
isEncrypted = false,
|
||||
enableEncryptionResult = { Result.success(Unit) }
|
||||
)
|
||||
val room = FakeMatrixRoom(enableEncryptionResult = { Result.success(Unit) })
|
||||
val presenter = createIdentityChangeStatePresenter(room)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -85,8 +85,9 @@ class IdentityChangeStatePresenterTest {
|
|||
)
|
||||
// No item emitted.
|
||||
expectNoEvents()
|
||||
// Room become encrypted.
|
||||
room.enableEncryption()
|
||||
// Room becomes encrypted.
|
||||
room.givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||
|
||||
val finalItem = awaitItem()
|
||||
assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1)
|
||||
val value = finalItem.roomMemberIdentityStateChanges.first()
|
||||
|
|
@ -99,7 +100,7 @@ class IdentityChangeStatePresenterTest {
|
|||
@Test
|
||||
fun `present - when the room emits identity change, the presenter emits new state with member details`() =
|
||||
runTest {
|
||||
val room = FakeMatrixRoom(isEncrypted = true).apply {
|
||||
val room = FakeMatrixRoom().apply {
|
||||
givenRoomMembersState(
|
||||
MatrixRoomMembersState.Ready(
|
||||
listOf(
|
||||
|
|
@ -110,6 +111,7 @@ class IdentityChangeStatePresenterTest {
|
|||
).toImmutableList()
|
||||
)
|
||||
)
|
||||
givenRoomInfo(aRoomInfo(isEncrypted = true))
|
||||
}
|
||||
val presenter = createIdentityChangeStatePresenter(room)
|
||||
presenter.test {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
|||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||
|
|
@ -998,7 +999,6 @@ class MessageComposerPresenterTest {
|
|||
val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN)
|
||||
var canUserTriggerRoomNotificationResult = true
|
||||
val room = FakeMatrixRoom(
|
||||
isDirect = false,
|
||||
canUserTriggerRoomNotificationResult = { Result.success(canUserTriggerRoomNotificationResult) },
|
||||
typingNoticeResult = { Result.success(Unit) }
|
||||
).apply {
|
||||
|
|
@ -1007,6 +1007,7 @@ class MessageComposerPresenterTest {
|
|||
persistentListOf(currentUser, invitedUser, bob, david),
|
||||
)
|
||||
)
|
||||
givenRoomInfo(aRoomInfo(isDirect = false))
|
||||
}
|
||||
val flagsService = FakeFeatureFlagService(
|
||||
mapOf(
|
||||
|
|
@ -1060,9 +1061,6 @@ class MessageComposerPresenterTest {
|
|||
val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN)
|
||||
val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN)
|
||||
val room = FakeMatrixRoom(
|
||||
isDirect = true,
|
||||
activeMemberCount = 2,
|
||||
isEncrypted = true,
|
||||
canUserTriggerRoomNotificationResult = { Result.success(true) },
|
||||
typingNoticeResult = { Result.success(Unit) }
|
||||
).apply {
|
||||
|
|
@ -1071,6 +1069,12 @@ class MessageComposerPresenterTest {
|
|||
persistentListOf(currentUser, invitedUser, bob, david),
|
||||
)
|
||||
)
|
||||
givenRoomInfo(
|
||||
aRoomInfo(
|
||||
isDirect = true,
|
||||
activeMembersCount = 2,
|
||||
)
|
||||
)
|
||||
}
|
||||
val flagsService = FakeFeatureFlagService(
|
||||
mapOf(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue