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 -> viewModel.isEndToEndEncrypted.observe(viewLifecycleOwner) { encrypted ->
if (encrypted) { if (encrypted) {
binding.eventsList.addItemDecoration(headerItemDecoration) binding.eventsList.addItemDecoration(headerItemDecoration)
@ -1369,6 +1375,29 @@ open class ConversationFragment : SlidingPaneChildFragment() {
dialog.show() 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) { private fun openFileInAnotherApp(path: String, mime: String) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
val contentUri: Uri = val contentUri: Uri =

View file

@ -184,6 +184,12 @@ class ConversationInfoFragment : SlidingPaneChildFragment() {
} }
} }
viewModel.confirmGroupCallEvent.observe(viewLifecycleOwner) {
it.consume {
showConfirmGroupCallPopup()
}
}
sharedViewModel.listOfSelectedSipUrisEvent.observe(viewLifecycleOwner) { sharedViewModel.listOfSelectedSipUrisEvent.observe(viewLifecycleOwner) {
it.consume { list -> it.consume { list ->
Log.i("$TAG Found [${list.size}] new participants to add to the group, let's do it") 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() 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) { private fun copyAddressToClipboard(value: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("SIP address", value)) clipboard.setPrimaryClip(ClipData.newPlainText("SIP address", value))

View file

@ -23,11 +23,17 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.Factory import org.linphone.core.Factory
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel import org.linphone.ui.GenericViewModel
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
abstract class AbstractConversationViewModel : GenericViewModel() { abstract class AbstractConversationViewModel : GenericViewModel() {
companion object { companion object {
@ -38,6 +44,10 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
val confirmGroupCallEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
lateinit var chatRoom: ChatRoom lateinit var chatRoom: ChatRoom
lateinit var localSipUri: String lateinit var localSipUri: String
@ -48,6 +58,47 @@ abstract class AbstractConversationViewModel : GenericViewModel() {
return ::chatRoom.isInitialized 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 @WorkerThread
open fun beforeNotifyingChatRoomFound(sameOne: Boolean) { 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.Address
import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoomListenerStub import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.EventLog import org.linphone.core.EventLog
import org.linphone.core.Factory import org.linphone.core.Factory
import org.linphone.core.Friend import org.linphone.core.Friend
import org.linphone.core.Participant import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.ParticipantModel import org.linphone.ui.main.chat.model.ParticipantModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel 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 { private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread @WorkerThread
override fun onContactsLoaded() { 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 @UiThread
fun scheduleMeeting() { fun scheduleMeeting() {
coreContext.postOnCoreThread { coreContext.postOnCoreThread {
@ -637,42 +575,6 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
participants.postValue(participantsList) 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 @WorkerThread
private fun getParticipant(eventLog: EventLog): String { private fun getParticipant(eventLog: EventLog): String {
val participantAddress = eventLog.participantAddress 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
import org.linphone.core.ChatRoom.HistoryFilter import org.linphone.core.ChatRoom.HistoryFilter
import org.linphone.core.ChatRoomListenerStub import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.ConferenceScheduler
import org.linphone.core.ConferenceSchedulerListenerStub
import org.linphone.core.EventLog import org.linphone.core.EventLog
import org.linphone.core.Factory
import org.linphone.core.Friend import org.linphone.core.Friend
import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.SearchDirection import org.linphone.core.SearchDirection
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.EventLogModel 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 { private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread @WorkerThread
override fun onContactsLoaded() { 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 @UiThread
fun markAsRead() { fun markAsRead() {
coreContext.postOnCoreThread { 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 @UiThread
fun copyFileToUri(filePath: String, dest: Uri) { fun copyFileToUri(filePath: String, dest: Uri) {
val source = Uri.parse(FileUtils.getProperFilePath(filePath)) 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.DialogRemoveCallLogsBinding
import org.linphone.databinding.DialogRemoveConversationHistoryBinding import org.linphone.databinding.DialogRemoveConversationHistoryBinding
import org.linphone.databinding.DialogSetOrEditGroupSubjectBindingImpl import org.linphone.databinding.DialogSetOrEditGroupSubjectBindingImpl
import org.linphone.databinding.DialogStartGroupCallFromConversationBinding
import org.linphone.databinding.DialogUpdateAccountPasswordAfterRegisterFailureBinding import org.linphone.databinding.DialogUpdateAccountPasswordAfterRegisterFailureBinding
import org.linphone.databinding.DialogUpdateAccountPasswordBinding import org.linphone.databinding.DialogUpdateAccountPasswordBinding
import org.linphone.databinding.DialogUpdateAvailableBinding import org.linphone.databinding.DialogUpdateAvailableBinding
@ -281,6 +282,22 @@ class DialogUtils {
return getDialog(context, binding) 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 @UiThread
fun getDeleteConversationHistoryConfirmationDialog( fun getDeleteConversationHistoryConfirmationDialog(
context: Context, context: Context,

View file

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

View file

@ -113,7 +113,7 @@
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginEnd="15dp" android:layout_marginEnd="15dp"
android:enabled="@{!viewModel.emptySubject}" 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_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background" app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel" 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_marginTop="16dp"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginEnd="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_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background" app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel" app:layout_constraintTop_toBottomOf="@id/cancel"

View file

@ -123,7 +123,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginEnd="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_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background" app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel" app:layout_constraintTop_toBottomOf="@id/cancel"

View file

@ -43,6 +43,7 @@
<string name="dialog_no">Non</string> <string name="dialog_no">Non</string>
<string name="dialog_yes">Oui</string> <string name="dialog_yes">Oui</string>
<string name="dialog_remove">Retirer</string> <string name="dialog_remove">Retirer</string>
<string name="dialog_confirm">Confirmer</string>
<!-- Related to Android notifications --> <!-- Related to Android notifications -->
<string name="notification_channel_call_name">&appName; notifications d\'appels en cours</string> <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_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_cant_be_empty_error">Un nom est obligatoire</string>
<string name="conversation_dialog_subject_hint">Nom de la conversation</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_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_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> <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_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_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_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_created">Vous avez rejoint le groupe</string>
<string name="conversation_event_conference_destroyed">Vous avez quitté 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_no">No</string>
<string name="dialog_yes">Yes</string> <string name="dialog_yes">Yes</string>
<string name="dialog_remove">Remove</string> <string name="dialog_remove">Remove</string>
<string name="dialog_confirm">Confirm</string>
<!-- Related to Android notifications --> <!-- Related to Android notifications -->
<string name="notification_channel_call_name">&appName; active calls notifications</string> <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_edit_subject">Edit conversation subject</string>
<string name="conversation_dialog_subject_cant_be_empty_error">Subject is mandatory</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_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_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_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> <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_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_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_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_created">You have joined the group</string>
<string name="conversation_event_conference_destroyed">You have left the group</string> <string name="conversation_event_conference_destroyed">You have left the group</string>