Added confirmation dialog before starting a group call from a group conversation

This commit is contained in:
Sylvain Berfini 2024-09-23 14:43:34 +02:00
parent eaa498f1ad
commit 63cb7d6630
13 changed files with 299 additions and 203 deletions

View file

@ -512,6 +512,12 @@ open class ConversationFragment : SlidingPaneChildFragment() {
}
}
viewModel.confirmGroupCallEvent.observe(viewLifecycleOwner) {
it.consume {
showConfirmGroupCallPopup()
}
}
viewModel.isEndToEndEncrypted.observe(viewLifecycleOwner) { encrypted ->
if (encrypted) {
binding.eventsList.addItemDecoration(headerItemDecoration)
@ -1369,6 +1375,29 @@ open class ConversationFragment : SlidingPaneChildFragment() {
dialog.show()
}
private fun showConfirmGroupCallPopup() {
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getConfirmGroupCallDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.startGroupCall()
dialog.dismiss()
}
}
dialog.show()
}
private fun openFileInAnotherApp(path: String, mime: String) {
val intent = Intent(Intent.ACTION_VIEW)
val contentUri: Uri =

View file

@ -184,6 +184,12 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
}
}
viewModel.confirmGroupCallEvent.observe(viewLifecycleOwner) {
it.consume {
showConfirmGroupCallPopup()
}
}
sharedViewModel.listOfSelectedSipUrisEvent.observe(viewLifecycleOwner) {
it.consume { list ->
Log.i("$TAG Found [${list.size}] new participants to add to the group, let's do it")
@ -428,6 +434,29 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
dialog.show()
}
private fun showConfirmGroupCallPopup() {
val model = ConfirmationDialogModel()
val dialog = DialogUtils.getConfirmGroupCallDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.startGroupCall()
dialog.dismiss()
}
}
dialog.show()
}
private fun copyAddressToClipboard(value: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("SIP address", value))

View file

@ -23,11 +23,17 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.ChatRoom
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.Factory
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
abstract class AbstractConversationViewModel : GenericViewModel() {
companion object {
@ -38,6 +44,10 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
MutableLiveData<Event<Boolean>>()
}
val confirmGroupCallEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
lateinit var chatRoom: ChatRoom
lateinit var localSipUri: String
@ -48,6 +58,47 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
return ::chatRoom.isInitialized
}
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) {
conferenceScheduler.removeListener(this)
val conferenceAddress = conferenceScheduler.info?.uri
if (conferenceAddress != null) {
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
} else if (state == ConferenceScheduler.State.Error) {
conferenceScheduler.removeListener(this)
Log.e("$TAG Failed to create group call!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
}
}
@WorkerThread
open fun beforeNotifyingChatRoomFound(sameOne: Boolean) {
}
@ -128,4 +179,59 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
}
}
}
@UiThread
fun startCall() {
coreContext.postOnCoreThread {
if (LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.participants.size >= 2) {
confirmGroupCallEvent.postValue(Event(true))
} else {
val firstParticipant = chatRoom.participants.firstOrNull()
val address = firstParticipant?.address
if (address != null) {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)
} else {
Log.e("$TAG Failed to find participant to call!")
}
}
}
}
@UiThread
fun startGroupCall() {
coreContext.postOnCoreThread { core ->
val account = core.defaultAccount
if (account == null) {
Log.e(
"$TAG No default account found, can't create group call!"
)
return@postOnCoreThread
}
val conferenceInfo = Factory.instance().createConferenceInfo()
conferenceInfo.organizer = account.params.identityAddress
conferenceInfo.subject = chatRoom.subject
val participants = arrayOfNulls<ParticipantInfo>(chatRoom.participants.size)
var index = 0
for (participant in chatRoom.participants) {
val info = Factory.instance().createParticipantInfo(participant.address)
// For meetings, all participants must have Speaker role
info?.role = Participant.Role.Speaker
participants[index] = info
index += 1
}
conferenceInfo.setParticipantInfos(participants)
Log.i(
"$TAG Creating group call with subject ${conferenceInfo.subject} and ${participants.size} participant(s)"
)
val conferenceScheduler = core.createConferenceScheduler()
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.account = account
// Will trigger the conference creation/update automatically
conferenceScheduler.info = conferenceInfo
}
}
}

View file

@ -30,13 +30,10 @@ import org.linphone.contacts.ContactsManager
import org.linphone.core.Address
import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.EventLog
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.ParticipantModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
@ -196,47 +193,6 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
}
}
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) {
conferenceScheduler.removeListener(this)
val conferenceAddress = conferenceScheduler.info?.uri
if (conferenceAddress != null) {
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
} else if (state == ConferenceScheduler.State.Error) {
conferenceScheduler.removeListener(this)
Log.e("$TAG Failed to create group call!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
}
}
private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
@ -304,24 +260,6 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
}
}
@UiThread
fun call() {
coreContext.postOnCoreThread { core ->
if (LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.participants.size >= 2) {
createGroupCall()
} else {
val firstParticipant = chatRoom.participants.firstOrNull()
val address = firstParticipant?.address
if (address != null) {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)
} else {
Log.e("$TAG Failed to find participant to call!")
}
}
}
}
@UiThread
fun scheduleMeeting() {
coreContext.postOnCoreThread {
@ -637,42 +575,6 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
participants.postValue(participantsList)
}
@WorkerThread
private fun createGroupCall() {
val core = coreContext.core
val account = core.defaultAccount
if (account == null) {
Log.e(
"$TAG No default account found, can't create group call!"
)
return
}
val conferenceInfo = Factory.instance().createConferenceInfo()
conferenceInfo.organizer = account.params.identityAddress
conferenceInfo.subject = subject.value
val participants = arrayOfNulls<ParticipantInfo>(chatRoom.participants.size)
var index = 0
for (participant in chatRoom.participants) {
val info = Factory.instance().createParticipantInfo(participant.address)
// For meetings, all participants must have Speaker role
info?.role = Participant.Role.Speaker
participants[index] = info
index += 1
}
conferenceInfo.setParticipantInfos(participants)
Log.i(
"$TAG Creating group call with subject ${subject.value} and ${participants.size} participant(s)"
)
val conferenceScheduler = core.createConferenceScheduler()
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.account = account
// Will trigger the conference creation/update automatically
conferenceScheduler.info = conferenceInfo
}
@WorkerThread
private fun getParticipant(eventLog: EventLog): String {
val participantAddress = eventLog.participantAddress

View file

@ -36,13 +36,8 @@ import org.linphone.core.ChatMessageReaction
import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoom.HistoryFilter
import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.EventLog
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.SearchDirection
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.EventLogModel
@ -281,47 +276,6 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
}
}
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) {
conferenceScheduler.removeListener(this)
val conferenceAddress = conferenceScheduler.info?.uri
if (conferenceAddress != null) {
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
} else if (state == ConferenceScheduler.State.Error) {
conferenceScheduler.removeListener(this)
Log.e("$TAG Failed to create group call!")
showRedToastEvent.postValue(
Event(
Pair(
R.string.conference_failed_to_create_group_call_toast,
R.drawable.warning_circle
)
)
)
}
}
}
private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
@ -463,22 +417,6 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
}
}
@UiThread
fun startCall() {
coreContext.postOnCoreThread { core ->
if (LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.participants.size >= 2) {
createGroupCall()
} else {
val firstParticipant = chatRoom.participants.firstOrNull()
val address = firstParticipant?.address
if (address != null) {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
coreContext.startAudioCall(address)
}
}
}
}
@UiThread
fun markAsRead() {
coreContext.postOnCoreThread {
@ -987,42 +925,6 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
}
}
@WorkerThread
private fun createGroupCall() {
val core = coreContext.core
val account = core.defaultAccount
if (account == null) {
Log.e(
"$TAG No default account found, can't create group call!"
)
return
}
val conferenceInfo = Factory.instance().createConferenceInfo()
conferenceInfo.organizer = account.params.identityAddress
conferenceInfo.subject = subject.value
val participants = arrayOfNulls<ParticipantInfo>(chatRoom.participants.size)
var index = 0
for (participant in chatRoom.participants) {
val info = Factory.instance().createParticipantInfo(participant.address)
// For meetings, all participants must have Speaker role
info?.role = Participant.Role.Speaker
participants[index] = info
index += 1
}
conferenceInfo.setParticipantInfos(participants)
Log.i(
"$TAG Creating group call with subject ${subject.value} and ${participants.size} participant(s)"
)
val conferenceScheduler = core.createConferenceScheduler()
conferenceScheduler.addListener(conferenceSchedulerListener)
conferenceScheduler.account = account
// Will trigger the conference creation/update automatically
conferenceScheduler.info = conferenceInfo
}
@UiThread
fun copyFileToUri(filePath: String, dest: Uri) {
val source = Uri.parse(FileUtils.getProperFilePath(filePath))

View file

@ -51,6 +51,7 @@ import org.linphone.databinding.DialogRemoveAllCallLogsBinding
import org.linphone.databinding.DialogRemoveCallLogsBinding
import org.linphone.databinding.DialogRemoveConversationHistoryBinding
import org.linphone.databinding.DialogSetOrEditGroupSubjectBindingImpl
import org.linphone.databinding.DialogStartGroupCallFromConversationBinding
import org.linphone.databinding.DialogUpdateAccountPasswordAfterRegisterFailureBinding
import org.linphone.databinding.DialogUpdateAccountPasswordBinding
import org.linphone.databinding.DialogUpdateAvailableBinding
@ -281,6 +282,22 @@ class DialogUtils {
return getDialog(context, binding)
}
@UiThread
fun getConfirmGroupCallDialog(
context: Context,
viewModel: ConfirmationDialogModel
): Dialog {
val binding: DialogStartGroupCallFromConversationBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_start_group_call_from_conversation,
null,
false
)
binding.viewModel = viewModel
return getDialog(context, binding)
}
@UiThread
fun getDeleteConversationHistoryConfirmationDialog(
context: Context,

View file

@ -224,7 +224,7 @@
android:layout_height="56dp"
android:layout_marginTop="40dp"
android:background="@drawable/circle_light_blue_button_background"
android:onClick="@{() -> viewModel.call()}"
android:onClick="@{() -> viewModel.startCall()}"
android:padding="16dp"
android:src="@drawable/phone"
android:contentDescription="@string/content_description_call_start"
@ -240,7 +240,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:onClick="@{() -> viewModel.call()}"
android:onClick="@{() -> viewModel.startCall()}"
android:text="@string/conversation_action_call"
android:textSize="14sp"
android:visibility="@{viewModel.isReadOnly ? View.GONE : View.VISIBLE}"

View file

@ -113,7 +113,7 @@
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:enabled="@{!viewModel.emptySubject}"
android:text="@string/conversation_dialog_edit_subject_confirm_button"
android:text="@string/dialog_confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"

View file

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.main.history.model.ConfirmationDialogModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.dismiss()}"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/dialog_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:src="@drawable/shape_dialog_background"
android:contentDescription="@null"
app:layout_constraintWidth_max="@dimen/dialog_max_width"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:paddingTop="@dimen/dialog_top_bottom_margin"
android:text="@string/conversation_info_confirm_start_group_call_dialog_title"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="10dp"
android:text="@string/conversation_info_confirm_start_group_call_dialog_message"
android:textSize="14sp"
android:autoLink="web"
android:textColorLink="?attr/color_main1_500"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
style="@style/secondary_button_label_style"
android:id="@+id/cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/confirm"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.confirm()}"
style="@style/primary_button_label_style"
android:id="@+id/confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/dialog_confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"
app:layout_constraintBottom_toTopOf="@id/anchor"/>
<View
android:id="@+id/anchor"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_top_bottom_margin"
app:layout_constraintTop_toBottomOf="@id/confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -107,7 +107,7 @@
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/conversation_dialog_edit_subject_confirm_button"
android:text="@string/dialog_confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"

View file

@ -123,7 +123,7 @@
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:text="@string/conversation_dialog_edit_subject_confirm_button"
android:text="@string/dialog_confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"

View file

@ -43,6 +43,7 @@
<string name="dialog_no">Non</string>
<string name="dialog_yes">Oui</string>
<string name="dialog_remove">Retirer</string>
<string name="dialog_confirm">Confirmer</string>
<!-- Related to Android notifications -->
<string name="notification_channel_call_name">&appName; notifications d\'appels en cours</string>
@ -444,7 +445,6 @@
<string name="conversation_dialog_edit_subject">Renommer la conversation</string>
<string name="conversation_dialog_subject_cant_be_empty_error">Un nom est obligatoire</string>
<string name="conversation_dialog_subject_hint">Nom de la conversation</string>
<string name="conversation_dialog_edit_subject_confirm_button">Confirmer</string>
<string name="conversation_dialog_open_or_export_file_title">Ouvrir ou sauvegarder le fichier ?</string>
<string name="conversation_dialog_open_or_export_file_message">&appName; ne peut ouvrir ce fichier.\n\nVoulez-vous l\'ouvrir dans une autre app (si possible), ou le sauvegarder sur votre appareil ?</string>
<string name="conversation_dialog_open_file_label">Ouvrir le fichier</string>
@ -485,6 +485,9 @@
<string name="conversation_info_participant_no_longer_has_admin_rights_toast">%s is no longer admin</string>
<string name="conversation_info_cant_find_contact_to_display_toast">Contact non trouvé</string>
<string name="conversation_info_no_address_to_add_to_contact_toast">Aucune adresse à ajouter au contact</string>
<string name="conversation_info_confirm_start_group_call_dialog_title">Démarrer un appel de groupe ?</string>
<string name="conversation_info_confirm_start_group_call_dialog_message">Tous les participants de la conversation recevront un appel.</string>
<string name="conversation_info_confirm_start_group_call_dialog_button">Démarrer l\'appel de groupe</string>
<string name="conversation_event_conference_created">Vous avez rejoint le groupe</string>
<string name="conversation_event_conference_destroyed">Vous avez quitté le groupe</string>

View file

@ -79,6 +79,7 @@
<string name="dialog_no">No</string>
<string name="dialog_yes">Yes</string>
<string name="dialog_remove">Remove</string>
<string name="dialog_confirm">Confirm</string>
<!-- Related to Android notifications -->
<string name="notification_channel_call_name">&appName; active calls notifications</string>
@ -482,7 +483,6 @@
<string name="conversation_dialog_edit_subject">Edit conversation subject</string>
<string name="conversation_dialog_subject_cant_be_empty_error">Subject is mandatory</string>
<string name="conversation_dialog_subject_hint">Conversation subject</string>
<string name="conversation_dialog_edit_subject_confirm_button">Confirm</string>
<string name="conversation_dialog_open_or_export_file_title">Open or export file?</string>
<string name="conversation_dialog_open_or_export_file_message">&appName; can\'t open this file.\n\nDo you want to open it in another app (if possible), or export it on your device?</string>
<string name="conversation_dialog_open_file_label">Open file</string>
@ -523,6 +523,9 @@
<string name="conversation_info_participant_no_longer_has_admin_rights_toast">%s is no longer admin</string>
<string name="conversation_info_cant_find_contact_to_display_toast">Contact was not found</string>
<string name="conversation_info_no_address_to_add_to_contact_toast">No address to add to contact</string>
<string name="conversation_info_confirm_start_group_call_dialog_title">Start a group call?</string>
<string name="conversation_info_confirm_start_group_call_dialog_message">All participants will receive a call.</string>
<string name="conversation_info_confirm_start_group_call_dialog_button">Start a group call</string>
<string name="conversation_event_conference_created">You have joined the group</string>
<string name="conversation_event_conference_destroyed">You have left the group</string>