Reworked how meetings are cancelled/deleted from user perspective

This commit is contained in:
Sylvain Berfini 2025-08-27 12:12:36 +02:00
parent e9cc03891b
commit ce13d4c7d4
12 changed files with 190 additions and 215 deletions

View file

@ -223,14 +223,16 @@ class MeetingFragment : SlidingPaneChildFragment() {
)
val isUserOrganizer = viewModel.isEditable.value == true && viewModel.isCancelled.value == false
popupView.cancelInsteadOfDelete = isUserOrganizer
val hasNotStartedYet = viewModel.hasNotStartedYet.value == true
val showCancelActionInsteadOfDelete = isUserOrganizer && hasNotStartedYet
popupView.cancelInsteadOfDelete = showCancelActionInsteadOfDelete
popupView.setDeleteClickListener {
if (isUserOrganizer) {
// In case we are organizer of the meeting, ask user confirmation before cancelling it
if (isUserOrganizer && hasNotStartedYet) {
Log.i("$TAG Meeting start hasn't started yet and we are the organizer, asking user if it should be cancelled")
showCancelMeetingDialog()
} else {
// If we're not organizer, ask user confirmation of removing itself from participants & deleting it locally
showDeleteMeetingDialog()
Log.i("$TAG Deleting meeting [${viewModel.sipUri}]")
viewModel.delete()
}
popupWindow.dismiss()
}
@ -299,28 +301,16 @@ class MeetingFragment : SlidingPaneChildFragment() {
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.cancel()
Log.i("$TAG Cancelling meeting [${viewModel.sipUri}] and sending notification to participants")
viewModel.cancel(true)
dialog.dismiss()
}
}
dialog.show()
}
private fun showDeleteMeetingDialog() {
Log.i("$TAG Meeting is not editable or already cancelled, asking whether to delete it or not")
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getDeleteMeetingDialog(requireContext(), model)
model.dismissEvent.observe(viewLifecycleOwner) {
model.alternativeChoiceEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.delete()
Log.i("$TAG Cancelling meeting [${viewModel.sipUri}] without notifying participants")
viewModel.cancel(false)
dialog.dismiss()
}
}

View file

@ -166,7 +166,7 @@ class MeetingsListFragment : AbstractMainFragment() {
listViewModel.fetchInProgress.value = false
}
listViewModel.conferenceCancelledEvent.observe(viewLifecycleOwner) {
listViewModel.cancelMeetingViewModel.conferenceCancelledEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Meeting has been cancelled successfully, deleting it")
(requireActivity() as GenericActivity).showGreenToast(
@ -188,24 +188,20 @@ class MeetingsListFragment : AbstractMainFragment() {
adapter.meetingLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val isUserOrganizer = model.isMyselfOrganizer && !model.isCancelled
val showCancelActionInsteadOfDelete = isUserOrganizer && model.hasNotStartedYet
val modalBottomSheet = MeetingsMenuDialogFragment(
isUserOrganizer,
showCancelActionInsteadOfDelete,
{ // onDismiss
adapter.resetSelection()
},
{ // onDelete
if (isUserOrganizer) {
if (showCancelActionInsteadOfDelete) {
Log.i("$TAG Meeting start hasn't started yet and we are the organizer, asking user if it should be cancelled")
showCancelMeetingDialog(model)
} else {
showDeleteMeetingDialog(model)
/*Log.i("$TAG Deleting meeting [${model.id}]")
Log.i("$TAG Deleting meeting [${model.id}]")
model.delete()
listViewModel.applyFilter()
(requireActivity() as GenericActivity).showGreenToast(
getString(R.string.meeting_info_deleted_toast),
R.drawable.trash_simple
)*/
}
}
)
@ -335,42 +331,22 @@ class MeetingsListFragment : AbstractMainFragment() {
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Cancelling meeting [${meetingModel.id}]")
Log.i("$TAG Cancelling meeting [${meetingModel.id}] and sending notification to participants")
meetingViewModelBeingCancelled = meetingModel
listViewModel.cancelMeeting(meetingModel.conferenceInfo)
listViewModel.cancelMeetingViewModel.cancelMeeting(meetingModel.conferenceInfo, true)
dialog.dismiss()
}
}
dialog.show()
}
private fun showDeleteMeetingDialog(meetingModel: MeetingModel) {
Log.i("$TAG Meeting is not editable or already cancelled, asking whether to deleting it or not")
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getDeleteMeetingDialog(requireContext(), model)
model.dismissEvent.observe(viewLifecycleOwner) {
model.alternativeChoiceEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Cancelling meeting [${meetingModel.id}] without notifying participants")
meetingViewModelBeingCancelled = meetingModel
listViewModel.cancelMeetingViewModel.cancelMeeting(meetingModel.conferenceInfo, false)
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Deleting meeting [${meetingModel.id}]")
meetingModel.delete()
listViewModel.applyFilter()
dialog.dismiss()
(requireActivity() as GenericActivity).showGreenToast(
getString(R.string.meeting_info_deleted_toast),
R.drawable.trash_simple
)
}
}
dialog.show()
}
}

View file

@ -33,7 +33,7 @@ import org.linphone.databinding.MeetingsListLongPressMenuBinding
@UiThread
class MeetingsMenuDialogFragment(
private val isUserOrganizer: Boolean,
private val showCancelActionInsteadOfDelete: Boolean,
private val onDismiss: (() -> Unit)? = null,
private val onDeleteMeeting: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
@ -65,7 +65,7 @@ class MeetingsMenuDialogFragment(
savedInstanceState: Bundle?
): View {
val view = MeetingsListLongPressMenuBinding.inflate(layoutInflater)
view.cancelInsteadOfDelete = isUserOrganizer
view.cancelInsteadOfDelete = showCancelActionInsteadOfDelete
view.setDeleteClickListener {
onDeleteMeeting?.invoke()

View file

@ -49,6 +49,8 @@ class MeetingModel
val isAfterToday = TimestampUtils.isAfterToday(timestamp)
val hasNotStartedYet = timestamp * 1000 > System.currentTimeMillis()
private val startTime = TimestampUtils.timeToString(timestamp)
private val endTime = TimestampUtils.timeToString(timestamp + (conferenceInfo.duration * 60))

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.viewmodel
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Address
import org.linphone.core.ConferenceInfo
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
open class CancelMeetingViewModel
@UiThread
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Cancel Meeting ViewModel]"
}
val operationInProgress = MutableLiveData<Boolean>()
val conferenceCancelledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private var sendNotificationForCancelledConference: Boolean = false
private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() {
override fun onStateChanged(
conferenceScheduler: ConferenceScheduler,
state: ConferenceScheduler.State?
) {
Log.i("$TAG Conference scheduler state is $state")
if (state == ConferenceScheduler.State.Ready) {
Log.i(
"$TAG Conference ${conferenceScheduler.info?.subject} has been cancelled"
)
if (sendNotificationForCancelledConference) {
Log.i("$TAG Sending cancelled meeting ICS to participants")
val params = LinphoneUtils.getChatRoomParamsToCancelMeeting()
if (params != null && !corePreferences.disableChat) {
conferenceScheduler.sendInvitations(params)
} else {
Log.e("$TAG Failed to get chat room params to send cancelled meeting ICS!")
operationInProgress.postValue(false)
}
} else {
operationInProgress.postValue(false)
conferenceCancelledEvent.postValue(Event(true))
}
} else if (state == ConferenceScheduler.State.Error) {
operationInProgress.postValue(false)
// TODO FIXME: show error to user
}
}
override fun onInvitationsSent(
conferenceScheduler: ConferenceScheduler,
failedInvitations: Array<out Address>?
) {
if (failedInvitations?.isNotEmpty() == true) {
// TODO FIXME: show error to user
for (address in failedInvitations) {
Log.e(
"$TAG Conference cancelled ICS wasn't sent to participant ${address.asStringUriOnly()}"
)
}
} else {
Log.i(
"$TAG Conference cancelled ICS successfully sent to all participants"
)
}
conferenceScheduler.removeListener(this)
operationInProgress.postValue(false)
conferenceCancelledEvent.postValue(Event(true))
}
}
init {
operationInProgress.value = false
}
@UiThread
fun cancelMeeting(conferenceInfo: ConferenceInfo, sendNotification: Boolean) {
coreContext.postOnCoreThread { core ->
Log.w("$TAG Cancelling conference info [${conferenceInfo.uri?.asStringUriOnly()}]")
sendNotificationForCancelledConference = sendNotification
operationInProgress.postValue(true)
val conferenceScheduler = LinphoneUtils.createConferenceScheduler(
LinphoneUtils.getDefaultAccount()
)
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.cancelConference(conferenceInfo)
}
}
}

View file

@ -24,24 +24,18 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import java.util.TimeZone
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Address
import org.linphone.core.ConferenceInfo
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.Factory
import org.linphone.core.Participant
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.meetings.model.ParticipantModel
import org.linphone.ui.main.meetings.model.TimeZoneModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class MeetingViewModel
@UiThread
constructor() : GenericViewModel() {
constructor() : CancelMeetingViewModel() {
companion object {
private const val TAG = "[Meeting ViewModel]"
}
@ -60,6 +54,8 @@ class MeetingViewModel
val timezone = MutableLiveData<String>()
val hasNotStartedYet = MutableLiveData<Boolean>()
val description = MutableLiveData<String>()
val speakers = MutableLiveData<ArrayList<ParticipantModel>>()
@ -73,65 +69,12 @@ class MeetingViewModel
val startTimeStamp = MutableLiveData<Long>()
val endTimeStamp = MutableLiveData<Long>()
val operationInProgress = MutableLiveData<Boolean>()
val conferenceCancelledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val conferenceInfoDeletedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() {
override fun onStateChanged(
conferenceScheduler: ConferenceScheduler,
state: ConferenceScheduler.State?
) {
Log.i("$TAG Conference scheduler state is $state")
if (state == ConferenceScheduler.State.Ready) {
Log.i(
"$TAG Conference ${conferenceScheduler.info?.subject} cancelled"
)
val params = LinphoneUtils.getChatRoomParamsToCancelMeeting()
if (params != null && !corePreferences.disableChat) {
conferenceScheduler.sendInvitations(params)
} else {
operationInProgress.postValue(false)
}
} else if (state == ConferenceScheduler.State.Error) {
operationInProgress.postValue(false)
}
}
override fun onInvitationsSent(
conferenceScheduler: ConferenceScheduler,
failedInvitations: Array<out Address>?
) {
if (failedInvitations?.isNotEmpty() == true) {
for (address in failedInvitations) {
Log.e(
"$TAG Conference cancelled ICS wasn't sent to participant ${address.asStringUriOnly()}"
)
}
} else {
Log.i(
"$TAG Conference cancelled ICS successfully sent to all participants"
)
}
conferenceScheduler.removeListener(this)
operationInProgress.postValue(false)
conferenceCancelledEvent.postValue(Event(true))
}
}
private lateinit var conferenceInfo: ConferenceInfo
init {
operationInProgress.value = false
}
@UiThread
fun findConferenceInfo(meeting: ConferenceInfo?, uri: String) {
coreContext.postOnCoreThread { core ->
@ -182,21 +125,6 @@ class MeetingViewModel
}
}
@UiThread
fun cancel() {
coreContext.postOnCoreThread { core ->
if (::conferenceInfo.isInitialized) {
Log.i("$TAG Cancelling conference information [$conferenceInfo]")
operationInProgress.postValue(true)
val conferenceScheduler = LinphoneUtils.createConferenceScheduler(
LinphoneUtils.getDefaultAccount()
)
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.cancelConference(conferenceInfo)
}
}
}
@UiThread
fun refreshInfo(uri: String) {
coreContext.postOnCoreThread { core ->
@ -243,6 +171,7 @@ class MeetingViewModel
endTimeStamp.postValue(end * 1000)
val displayedTimestamp = "$date | $startTime - $endTime"
dateTime.postValue(displayedTimestamp)
hasNotStartedYet.postValue(timestamp * 1000 > System.currentTimeMillis())
Log.i("$TAG Conference is scheduled for [$displayedTimestamp]")
timezone.postValue(TimeZoneModel(TimeZone.getDefault()).toString())
@ -311,4 +240,11 @@ class MeetingViewModel
speakers.postValue(speakersList)
participants.postValue(participantsList)
}
@UiThread
fun cancel(sendNotification: Boolean) {
if (::conferenceInfo.isInitialized) {
cancelMeeting(conferenceInfo, sendNotification)
}
}
}

View file

@ -23,21 +23,15 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Account
import org.linphone.core.AccountListenerStub
import org.linphone.core.Address
import org.linphone.core.ConferenceInfo
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.meetings.model.MeetingListItemModel
import org.linphone.ui.main.meetings.model.MeetingModel
import org.linphone.ui.main.viewmodel.AbstractMainViewModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class MeetingsListViewModel
@ -51,11 +45,7 @@ class MeetingsListViewModel
val fetchInProgress = MutableLiveData<Boolean>()
val operationInProgress = MutableLiveData<Boolean>()
val conferenceCancelledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val cancelMeetingViewModel = CancelMeetingViewModel()
private val coreListener = object : CoreListenerStub() {
@WorkerThread
@ -78,51 +68,7 @@ class MeetingsListViewModel
}
}
private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() {
override fun onStateChanged(
conferenceScheduler: ConferenceScheduler,
state: ConferenceScheduler.State?
) {
Log.i("$TAG Conference scheduler state is $state")
if (state == ConferenceScheduler.State.Ready) {
Log.i(
"$TAG Conference ${conferenceScheduler.info?.subject} cancelled"
)
val params = LinphoneUtils.getChatRoomParamsToCancelMeeting()
if (params != null && !corePreferences.disableChat) {
conferenceScheduler.sendInvitations(params)
} else {
operationInProgress.postValue(false)
}
} else if (state == ConferenceScheduler.State.Error) {
operationInProgress.postValue(false)
}
}
override fun onInvitationsSent(
conferenceScheduler: ConferenceScheduler,
failedInvitations: Array<out Address>?
) {
if (failedInvitations?.isNotEmpty() == true) {
for (address in failedInvitations) {
Log.e(
"$TAG Conference cancelled ICS wasn't sent to participant ${address.asStringUriOnly()}"
)
}
} else {
Log.i(
"$TAG Conference cancelled ICS successfully sent to all participants"
)
}
conferenceScheduler.removeListener(this)
operationInProgress.postValue(false)
conferenceCancelledEvent.postValue(Event(true))
}
}
init {
operationInProgress.value = false
fetchInProgress.value = true
coreContext.postOnCoreThread { core ->
@ -150,19 +96,6 @@ class MeetingsListViewModel
}
}
@UiThread
fun cancelMeeting(conferenceInfo: ConferenceInfo) {
coreContext.postOnCoreThread { core ->
Log.w("$TAG Cancelling conference info [${conferenceInfo.uri?.asStringUriOnly()}]")
operationInProgress.postValue(true)
val conferenceScheduler = LinphoneUtils.createConferenceScheduler(
LinphoneUtils.getDefaultAccount()
)
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.cancelConference(conferenceInfo)
}
}
@WorkerThread
private fun computeMeetingsListFromLocallyStoredInfo() {
var source = coreContext.core.defaultAccount?.conferenceInformationList

View file

@ -112,7 +112,7 @@
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
bind:visibility="@{viewModel.cancelMeetingViewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -37,7 +37,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:paddingTop="@dimen/dialog_top_bottom_margin"
android:text="@string/meeting_schedule_cancel_dialog_title"
android:text="@string/meeting_schedule_notify_cancel_dialog_title"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintStart_toStartOf="@id/dialog_background"
@ -52,7 +52,7 @@
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="10dp"
android:text="@string/meeting_schedule_cancel_dialog_message"
android:text="@string/meeting_schedule_notify_cancel_dialog_message"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
@ -62,7 +62,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.confirm()}"
style="@style/primary_dialog_button_label_style"
android:id="@+id/confirm"
android:id="@+id/send_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
@ -72,10 +72,26 @@
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toStartOf="@id/cancel"
app:layout_constraintEnd_toStartOf="@id/dont_send_notification"
app:layout_constraintTop_toTopOf="@id/cancel"
app:layout_constraintBottom_toBottomOf="@id/cancel"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.alternativeChoice()}"
style="@style/secondary_dialog_button_label_style"
android:id="@+id/dont_send_notification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_no"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/send_notification"
app:layout_constraintEnd_toStartOf="@id/cancel"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/anchor"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
style="@style/secondary_dialog_button_label_style"
@ -85,9 +101,9 @@
android:layout_marginTop="32dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_no"
android:text="@string/dialog_do_not_cancel"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@id/confirm"
app:layout_constraintStart_toEndOf="@id/dont_send_notification"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/anchor"/>

View file

@ -103,7 +103,7 @@
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
bind:visibility="@{viewModel.cancelMeetingViewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -42,6 +42,7 @@
<string name="dialog_remove">Retirer</string>
<string name="dialog_confirm">Confirmer</string>
<string name="dialog_understood">J\'ai compris</string>
<string name="dialog_do_not_cancel">Ne pas annuler</string>
<!-- Related to Android notifications -->
<string name="notification_channel_call_name">Notifications d\'appels en cours</string>
@ -643,8 +644,8 @@
<string name="meeting_info_not_found_toast">Réunion introuvable !</string>
<string name="meeting_schedule_description_title">Description</string>
<string name="meeting_schedule_edit_title">Modifier la réunion</string>
<string name="meeting_schedule_cancel_dialog_title">Annuler la réunion?</string>
<string name="meeting_schedule_cancel_dialog_message">Voulez-vous annuler la réunion et envoyer une notification aux participants ?</string>
<string name="meeting_schedule_notify_cancel_dialog_title">La réunion va être annulée</string>
<string name="meeting_schedule_notify_cancel_dialog_message">Voulez-vous envoyer une notification aux participants ?</string>
<string name="meeting_cancel_action_label">Annuler la réunion</string>
<string name="meeting_schedule_delete_dialog_title">Supprimer la réunion ?</string>
<string name="meeting_schedule_delete_dialog_message">Voulez-vous supprimer la réunion ?</string>

View file

@ -83,6 +83,7 @@
<string name="dialog_remove">Remove</string>
<string name="dialog_confirm">Confirm</string>
<string name="dialog_understood">Understood</string>
<string name="dialog_do_not_cancel">Do not cancel</string>
<!-- Related to Android notifications -->
<string name="notification_channel_call_name">Active calls notifications</string>
@ -686,8 +687,8 @@
<string name="meeting_info_not_found_toast">Meeting cannot be found!</string>
<string name="meeting_schedule_description_title">Description</string>
<string name="meeting_schedule_edit_title">Edit meeting</string>
<string name="meeting_schedule_cancel_dialog_title">Cancel the meeting?</string>
<string name="meeting_schedule_cancel_dialog_message">Do you want to cancel the meeting and send a notification to all participants?</string>
<string name="meeting_schedule_notify_cancel_dialog_title">Meeting will be cancelled</string>
<string name="meeting_schedule_notify_cancel_dialog_message">Do you want to send a notification to all participants?</string>
<string name="meeting_cancel_action_label">Cancel meeting</string>
<string name="meeting_schedule_delete_dialog_title">Delete the meeting?</string>
<string name="meeting_schedule_delete_dialog_message">Do you want to delete the meeting?</string>