From a32bbd450674d924212a8d669f6dcf97b520f6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 13 Aug 2026 12:08:50 +0200 Subject: [PATCH 1/4] feat: update conversation name when updating meeting [WPB-27617] --- .../com/wire/android/WireApplication.kt | 2 +- .../ui/create/NewMeetingErrorDialog.kt | 49 ++++++-- .../meetings/ui/create/NewMeetingScreen.kt | 7 +- .../meetings/ui/create/NewMeetingViewModel.kt | 30 ++++- .../meetings/src/main/res/values/strings.xml | 3 + .../ui/create/NewMeetingViewModelTest.kt | 109 +++++++++++++++++- kalium | 2 +- 7 files changed, 186 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index a53ddb057d4..9cf0a0ba9ab 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -458,7 +458,7 @@ class WireApplication : BaseApp() { internal companion object { fun minimalLoggerConfig() = KaliumLogger.Config( - KaliumLogLevel.WARN, + KaliumLogLevel.VERBOSE, listOf(platformLogWriter()) ) diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingErrorDialog.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingErrorDialog.kt index 8c470c994f3..f87866f989b 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingErrorDialog.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingErrorDialog.kt @@ -24,35 +24,64 @@ import com.wire.android.feature.meetings.ui.util.PreviewMultipleThemes import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.theme.WireTheme +import com.wire.kalium.logic.data.id.ConversationId import com.wire.android.ui.common.R as commonR @Composable fun NewMeetingErrorDialog( + error: NewMeetingState.SubmitError, type: NewMeetingType, + isSubmitting: Boolean, onDismiss: () -> Unit, + onRetryUpdateConversationName: (conversationId: ConversationId) -> Unit ) { // TODO: specific errors to be handled later, for now we just show single generic one - val (titleResId, descriptionResId) = when (type) { - NewMeetingType.MeetNow -> R.string.new_meeting_now_failure_title to R.string.new_meeting_now_failure_description - NewMeetingType.Schedule -> R.string.new_meeting_schedule_failure_title to R.string.new_meeting_schedule_failure_description - is NewMeetingType.Edit -> R.string.new_meeting_edit_failure_title to R.string.new_meeting_edit_failure_description + val (titleResId, descriptionResId) = when (error) { + is NewMeetingState.SubmitError.UpdateConversationNameFailure -> + R.string.new_meeting_edit_conversation_name_failure_title to R.string.new_meeting_edit_conversation_name_failure_description + + is NewMeetingState.SubmitError.Other -> when (type) { + NewMeetingType.MeetNow -> R.string.new_meeting_now_failure_title to R.string.new_meeting_now_failure_description + NewMeetingType.Schedule -> R.string.new_meeting_schedule_failure_title to R.string.new_meeting_schedule_failure_description + is NewMeetingType.Edit -> R.string.new_meeting_edit_failure_title to R.string.new_meeting_edit_failure_description + } } + WireDialog( title = stringResource(titleResId), text = stringResource(descriptionResId), onDismiss = onDismiss, buttonsHorizontalAlignment = false, - optionButton1Properties = WireDialogButtonProperties( - onClick = onDismiss, - text = stringResource(commonR.string.label_ok), - type = WireDialogButtonType.Primary, - ), + optionButton1Properties = when (error) { + is NewMeetingState.SubmitError.Other -> WireDialogButtonProperties( + onClick = onDismiss, + text = stringResource(commonR.string.label_ok), + type = WireDialogButtonType.Primary, + ) + + is NewMeetingState.SubmitError.UpdateConversationNameFailure -> WireDialogButtonProperties( + onClick = { + onRetryUpdateConversationName(error.conversationId) + }, + text = stringResource(R.string.new_meeting_edit_conversation_name_failure_action), + type = WireDialogButtonType.Primary, + state = if (isSubmitting) WireButtonState.Disabled else WireButtonState.Default, + loading = isSubmitting, + ) + } ) } @PreviewMultipleThemes @Composable private fun PreviewCreateGroupErrorDialogLackingConnection() = WireTheme { - NewMeetingErrorDialog(type = NewMeetingType.MeetNow, onDismiss = {}) + NewMeetingErrorDialog( + error = NewMeetingState.SubmitError.Other, + type = NewMeetingType.MeetNow, + isSubmitting = false, + onDismiss = {}, + onRetryUpdateConversationName = {}, + ) } diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingScreen.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingScreen.kt index 7f89fcb46ea..a123dd4ce3d 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingScreen.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingScreen.kt @@ -151,10 +151,13 @@ fun NewMeetingScreen( onRepeatingIntervalChanged = newMeetingViewModel::updateRepeatingInterval, ) - if (newMeetingViewModel.state.submitError != null) { + newMeetingViewModel.state.submitError?.let { submitError -> NewMeetingErrorDialog( + error = submitError, type = newMeetingViewModel.type, - onDismiss = newMeetingViewModel::dismissCreationError + isSubmitting = newMeetingViewModel.state.isSubmitting, + onDismiss = newMeetingViewModel::dismissCreationError, + onRetryUpdateConversationName = newMeetingViewModel::retryUpdateConversationName, ) } if (newMeetingViewModel.state.initialLoading == NewMeetingState.InitialLoadingState.Error) { diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt index b2cb1f7a62b..8f889761cd0 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt @@ -37,11 +37,14 @@ import com.wire.android.ui.common.ActionsManager import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.util.CurrentTimeProvider +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.meeting.Meeting import com.wire.kalium.logic.data.meeting.UpsertMeeting import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase +import com.wire.kalium.logic.feature.conversation.RenamingResult import com.wire.kalium.logic.feature.meeting.CreateNewMeetingUseCase import com.wire.kalium.logic.feature.meeting.GetNextMeetingOccurrenceUseCase import com.wire.kalium.logic.feature.meeting.UpdateMeetingUseCase @@ -79,6 +82,7 @@ interface NewMeetingViewModel : ActionsManager { fun submitCreation() {} fun submitUpdate() {} fun dismissCreationError() {} + fun retryUpdateConversationName(conversationId: ConversationId) {} companion object { const val MEETING_NAME_MAX_COUNT = 64 @@ -100,12 +104,14 @@ class NewMeetingViewModelImpl @AssistedInject constructor( private val updateMeeting: UpdateMeetingUseCase, private val getNextMeetingOccurrence: GetNextMeetingOccurrenceUseCase, private val observeConversationMembers: ObserveConversationMembersUseCase, + private val renameConversationUseCase: RenameConversationUseCase, private val contactMapper: ContactMapper, ) : ActionsViewModel(), NewMeetingViewModel { @AssistedFactory interface Factory { fun create(savedStateHandle: SavedStateHandle): NewMeetingViewModelImpl } + val navArgs: NewMeetingNavArgs = savedStateHandle.navArgs() override val type: NewMeetingType = navArgs.type override val titleTextState: TextFieldState = TextFieldState() @@ -279,7 +285,28 @@ class NewMeetingViewModelImpl @AssistedInject constructor( state = state.copy(isSubmitting = false, continueButtonEnabled = true) when (updateResult) { is UpdateMeetingUseCase.Result.Success -> sendAction(NewMeetingViewActions.Success) - is UpdateMeetingUseCase.Result.Failure -> state = state.copy(submitError = NewMeetingState.SubmitError.Other) + is UpdateMeetingUseCase.Result.Failure -> state = state.copy( + submitError = when (updateResult) { + is UpdateMeetingUseCase.Result.Failure.UpdateConversationNameFailure -> + NewMeetingState.SubmitError.UpdateConversationNameFailure(updateResult.conversationId) + + else -> NewMeetingState.SubmitError.Other + } + ) + } + } + } + } + + override fun retryUpdateConversationName(conversationId: ConversationId) { + viewModelScope.launch { + state = state.copy(isSubmitting = true, continueButtonEnabled = false) + renameConversationUseCase(conversationId = conversationId, conversationName = titleTextState.text.trim().toString()).let { + state = state.copy(isSubmitting = false, continueButtonEnabled = true) + when (it) { + is RenamingResult.Failure -> + state = state.copy(submitError = NewMeetingState.SubmitError.UpdateConversationNameFailure(conversationId)) + RenamingResult.Success -> sendAction(NewMeetingViewActions.Success) } } } @@ -334,6 +361,7 @@ data class NewMeetingState( } sealed interface SubmitError { + data class UpdateConversationNameFailure(val conversationId: ConversationId) : SubmitError data object Other : SubmitError // TODO Add more specific error types in the future } diff --git a/features/meetings/src/main/res/values/strings.xml b/features/meetings/src/main/res/values/strings.xml index 01fff91187f..a94ab964e6b 100644 --- a/features/meetings/src/main/res/values/strings.xml +++ b/features/meetings/src/main/res/values/strings.xml @@ -89,6 +89,9 @@ Something went wrong while updating the meeting. Please try again. Could not load meeting Something went wrong while loading the meeting for update. Please try again. + Could not update call name + The meeting title was updated, but the name shown in the call could not be updated. Please try again. + Retry Could not join meeting Something went wrong while joining the meeting. Please try again. diff --git a/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt b/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt index 959e3bebd01..333e7e00249 100644 --- a/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt +++ b/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt @@ -31,6 +31,7 @@ import com.wire.android.mapper.ContactMapper import com.wire.android.model.Contact import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.CurrentTimeProvider +import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.MemberDetails import com.wire.kalium.logic.data.id.ConversationId @@ -42,6 +43,8 @@ import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase +import com.wire.kalium.logic.feature.conversation.RenamingResult import com.wire.kalium.logic.feature.meeting.CreateNewMeetingUseCase import com.wire.kalium.logic.feature.meeting.GetNextMeetingOccurrenceUseCase import com.wire.kalium.logic.feature.meeting.UpdateMeetingUseCase @@ -373,7 +376,7 @@ class NewMeetingViewModelTest { Arrangement(dispatcher) .withNewMeetingType(editType) .withNextMeetingOccurrence(nextOccurrence) - .withUpdateMeetingResult(nextOccurrence.meeting.meetingId, UpdateMeetingUseCase.Result.Failure) + .withUpdateMeetingResult(nextOccurrence.meeting.meetingId, UpdateMeetingUseCase.Result.Failure.Other) ) enterTitle(viewModel, "Weekly sync") @@ -390,6 +393,102 @@ class NewMeetingViewModelTest { } } + @Test + fun givenConversationNameEditionFails_whenSubmitUpdateIsCalled_thenErrorIsShownAndSuccessActionIsNotSent() = runTest(dispatcher) { + val currentTime = Instant.parse("2026-01-01T12:00:00Z") + val editType = NewMeetingType.Edit(MeetingId("meeting-id", "domain")) + val nextOccurrence = MEETING_OCCURRENCE.copy( + meeting = MEETING_OCCURRENCE.meeting.copy( + startTime = currentTime + 1.hours, + endTime = currentTime + 2.hours, + recurrence = Meeting.Recurrence(frequency = Meeting.Recurrence.Frequency.DAILY, interval = 1L, until = null), + ), + occurrenceStartTime = currentTime + 1.hours, + occurrenceEndTime = currentTime + 2.hours, + ) + val (arrangement, viewModel) = arrangeViewModel( + Arrangement(dispatcher) + .withNewMeetingType(editType) + .withNextMeetingOccurrence(nextOccurrence) + .withUpdateMeetingResult( + nextOccurrence.meeting.meetingId, + UpdateMeetingUseCase.Result.Failure.UpdateConversationNameFailure(nextOccurrence.meeting.conversationId) + ) + ) + + enterTitle(viewModel, "Weekly sync") + + viewModel.actions.test { + viewModel.submitUpdate() + advanceUntilIdle() + + coVerify(exactly = 1) { arrangement.updateMeeting(editType.id, any()) } + expectNoEvents() + assertFalse(viewModel.state.isSubmitting) + assertEquals(true, viewModel.state.continueButtonEnabled) + assertEquals( + NewMeetingState.SubmitError.UpdateConversationNameFailure(nextOccurrence.meeting.conversationId), + viewModel.state.submitError + ) + } + } + + @Test + fun givenRetryUpdateConversationNameSucceeds_whenRetryUpdateConversationNameIsCalled_thenSuccessActionIsSent() = runTest(dispatcher) { + val conversationId = ConversationId("conversation-id", "domain") + val (arrangement, viewModel) = arrangeViewModel( + Arrangement(dispatcher) + .withRenameConversationResult(conversationId, "Weekly sync", RenamingResult.Success) + ) + + enterTitle(viewModel, " Weekly sync ") + + viewModel.actions.test { + viewModel.retryUpdateConversationName(conversationId) + advanceUntilIdle() + + coVerify(exactly = 1) { + arrangement.renameConversationUseCase( + conversationId = conversationId, + conversationName = "Weekly sync" + ) + } + assertNull(viewModel.state.submitError) + assertEquals(NewMeetingViewActions.Success, awaitItem()) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun givenRetryUpdateConversationNameFails_whenRetryUpdateConversationNameIsCalled_thenErrorIsShownAndSuccessActionIsNotSent() = + runTest(dispatcher) { + val conversationId = ConversationId("conversation-id", "domain") + val (arrangement, viewModel) = arrangeViewModel( + Arrangement(dispatcher) + .withRenameConversationResult( + conversationId = conversationId, + conversationName = "Weekly sync", + result = RenamingResult.Failure(CoreFailure.Unknown(RuntimeException("Failed to rename conversation"))) + ) + ) + + enterTitle(viewModel, " Weekly sync ") + + viewModel.actions.test { + viewModel.retryUpdateConversationName(conversationId) + advanceUntilIdle() + + coVerify(exactly = 1) { + arrangement.renameConversationUseCase( + conversationId = conversationId, + conversationName = "Weekly sync" + ) + } + expectNoEvents() + assertEquals(NewMeetingState.SubmitError.UpdateConversationNameFailure(conversationId), viewModel.state.submitError) + } + } + @Test fun givenInvalidTitle_whenSubmitCreationIsCalled_thenTitleErrorIsShownAndSuccessActionIsNotSent() = runTest(dispatcher) { val (arrangement, viewModel) = arrangeViewModel() @@ -563,6 +662,9 @@ class NewMeetingViewModelTest { @MockK lateinit var observeConversationMembers: ObserveConversationMembersUseCase + @MockK + lateinit var renameConversationUseCase: RenameConversationUseCase + @MockK lateinit var contactMapper: ContactMapper @@ -593,6 +695,10 @@ class NewMeetingViewModelTest { coEvery { updateMeeting(meetingId, any()) } returns result } + fun withRenameConversationResult(conversationId: ConversationId, conversationName: String, result: RenamingResult) = apply { + coEvery { renameConversationUseCase(conversationId, conversationName) } returns result + } + fun withNextMeetingOccurrence(nextMeetingOccurrence: MeetingOccurrence?) = apply { coEvery { getNextMeetingOccurrence(any(), any()) } returns nextMeetingOccurrence } @@ -616,6 +722,7 @@ class NewMeetingViewModelTest { updateMeeting = updateMeeting, getNextMeetingOccurrence = getNextMeetingOccurrence, observeConversationMembers = observeConversationMembers, + renameConversationUseCase = renameConversationUseCase, contactMapper = contactMapper, ) } diff --git a/kalium b/kalium index 65657c61a5a..ebf6d4fcd99 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 65657c61a5a574a4f1c1e8746ee6f6ec3a501550 +Subproject commit ebf6d4fcd99e9fb775bc0a90ca08969bb6904cc4 From 9528be4595453c158fb953e160fd704e52a269e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 13 Aug 2026 12:19:36 +0200 Subject: [PATCH 2/4] detekt --- .../android/feature/meetings/ui/create/NewMeetingViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt index 8f889761cd0..1f89937bb01 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/create/NewMeetingViewModel.kt @@ -97,6 +97,7 @@ class NewMeetingViewModelPreview( override val state: NewMeetingState = initialState(currentTimeProvider) } +@Suppress("TooManyFunctions") class NewMeetingViewModelImpl @AssistedInject constructor( @Assisted savedStateHandle: SavedStateHandle, override val currentTimeProvider: CurrentTimeProvider, From 60a3be40bb1af55ac33a8ee6a458ef23d232b7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 13 Aug 2026 12:24:22 +0200 Subject: [PATCH 3/4] add test for UpdateConversationNameFailure when editing --- .../ui/create/NewMeetingViewModelTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt b/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt index 333e7e00249..afbff0a8ae4 100644 --- a/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt +++ b/features/meetings/src/test/kotlin/com/wire/android/feature/meetings/ui/create/NewMeetingViewModelTest.kt @@ -433,6 +433,48 @@ class NewMeetingViewModelTest { } } + @Test + fun givenUpdateActionReturnsUpdateConversationNameFailure_whenSubmitUpdateIsCalled_thenReturnedConversationErrorIsShown() = + runTest(dispatcher) { + val currentTime = Instant.parse("2026-01-01T12:00:00Z") + val editType = NewMeetingType.Edit(MeetingId("meeting-id", "domain")) + val nextOccurrence = MEETING_OCCURRENCE.copy( + meeting = MEETING_OCCURRENCE.meeting.copy( + startTime = currentTime + 1.hours, + endTime = currentTime + 2.hours, + recurrence = Meeting.Recurrence(frequency = Meeting.Recurrence.Frequency.DAILY, interval = 1L, until = null), + ), + occurrenceStartTime = currentTime + 1.hours, + occurrenceEndTime = currentTime + 2.hours, + ) + val failedConversationId = ConversationId("failed-conversation-id", "domain") + val (arrangement, viewModel) = arrangeViewModel( + Arrangement(dispatcher) + .withNewMeetingType(editType) + .withNextMeetingOccurrence(nextOccurrence) + .withUpdateMeetingResult( + nextOccurrence.meeting.meetingId, + UpdateMeetingUseCase.Result.Failure.UpdateConversationNameFailure(failedConversationId) + ) + ) + + enterTitle(viewModel, "Weekly sync") + + viewModel.actions.test { + viewModel.submitUpdate() + advanceUntilIdle() + + coVerify(exactly = 1) { arrangement.updateMeeting(editType.id, any()) } + expectNoEvents() + assertFalse(viewModel.state.isSubmitting) + assertEquals(true, viewModel.state.continueButtonEnabled) + assertEquals( + NewMeetingState.SubmitError.UpdateConversationNameFailure(failedConversationId), + viewModel.state.submitError + ) + } + } + @Test fun givenRetryUpdateConversationNameSucceeds_whenRetryUpdateConversationNameIsCalled_thenSuccessActionIsSent() = runTest(dispatcher) { val conversationId = ConversationId("conversation-id", "domain") From c9ab46ce0f2f8f9ce1a8b5051332ca7b01939cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= Date: Thu, 13 Aug 2026 12:40:53 +0200 Subject: [PATCH 4/4] revert logger initial level --- app/src/main/kotlin/com/wire/android/WireApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index 9cf0a0ba9ab..a53ddb057d4 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -458,7 +458,7 @@ class WireApplication : BaseApp() { internal companion object { fun minimalLoggerConfig() = KaliumLogger.Config( - KaliumLogLevel.VERBOSE, + KaliumLogLevel.WARN, listOf(platformLogWriter()) )