Added broadcast mode for scheduled conferences

This commit is contained in:
Sylvain Berfini 2023-08-21 14:41:11 +02:00
parent 2334a0e6a8
commit 4b27d1180a
29 changed files with 660 additions and 50 deletions

View file

@ -63,6 +63,8 @@ class ChatMessageContentData(
val isConferenceSchedule = MutableLiveData<Boolean>()
val isConferenceUpdated = MutableLiveData<Boolean>()
val isConferenceCancelled = MutableLiveData<Boolean>()
val isBroadcast = MutableLiveData<Boolean>()
val isSpeaker = MutableLiveData<Boolean>()
val fileName = MutableLiveData<String>()
val filePath = MutableLiveData<String>()
@ -378,14 +380,26 @@ class ChatMessageContentData(
var participantsCount = conferenceInfo.participants.size
val organizer = conferenceInfo.organizer
var organizerFound = false
if (organizer != null) {
for (participant in conferenceInfo.participants) {
var allSpeaker = true
isSpeaker.value = true
for (info in conferenceInfo.participantInfos) {
val participant = info.address
if (participant.weakEqual(chatMessage.chatRoom.localAddress)) {
isSpeaker.value = info.role == Participant.Role.Speaker
}
if (info.role == Participant.Role.Listener) {
allSpeaker = false
}
if (organizer != null) {
if (participant.weakEqual(organizer)) {
organizerFound = true
break
}
}
}
isBroadcast.value = allSpeaker == false
if (!organizerFound) participantsCount += 1 // +1 for organizer
conferenceParticipantCount.value = String.format(
AppUtils.getString(R.string.conference_invite_participants_count),

View file

@ -19,15 +19,35 @@
*/
package org.linphone.activities.main.conference.data
import androidx.lifecycle.MutableLiveData
import org.linphone.contact.GenericContactData
import org.linphone.core.Address
import org.linphone.utils.LinphoneUtils
class ConferenceSchedulingParticipantData(
private val sipAddress: Address,
val sipAddress: Address,
val showLimeBadge: Boolean = false,
val showDivider: Boolean = true
val showDivider: Boolean = true,
val showBroadcastControls: Boolean = false,
val speaker: Boolean = false,
private val onAddedToSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null,
private val onRemovedFromSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null
) :
GenericContactData(sipAddress) {
val isSpeaker = MutableLiveData<Boolean>()
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress)
init {
isSpeaker.value = speaker
}
fun changeIsSpeaker() {
isSpeaker.value = isSpeaker.value == false
if (isSpeaker.value == true) {
onAddedToSpeakers?.invoke(this)
} else {
onRemovedFromSpeakers?.invoke(this)
}
}
}

View file

@ -25,6 +25,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.ConferenceInfo
import org.linphone.core.ConferenceInfo.State
import org.linphone.core.Participant
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
@ -45,9 +46,12 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val is
val participantsExpanded = MutableLiveData<String>()
val showDuration = MutableLiveData<Boolean>()
val isConferenceCancelled = MutableLiveData<Boolean>()
val isBroadcast = MutableLiveData<Boolean>()
val speakersExpanded = MutableLiveData<String>()
init {
expanded.value = false
isBroadcast.value = false
address.value = conferenceInfo.uri?.asStringUriOnly()
subject.value = conferenceInfo.subject
@ -141,24 +145,48 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val is
private fun computeParticipantsLists() {
var participantsListShort = ""
var participantsListExpanded = ""
var speakersListExpanded = ""
var allSpeaker = true
for (info in conferenceInfo.participantInfos) {
val participant = info.address
Log.i(
"[Scheduled Conference] Conference [${subject.value}] participant [${participant.asStringUriOnly()}] is a [${info.role}]"
)
for (participant in conferenceInfo.participants) {
val contact = coreContext.contactsManager.findContactByAddress(participant)
val name = if (contact != null) {
contact.name
} else {
LinphoneUtils.getDisplayName(
participant
)
LinphoneUtils.getDisplayName(participant)
}
val address = participant.asStringUriOnly()
participantsListShort += "$name, "
participantsListExpanded += "$name ($address)\n"
when (info.role) {
Participant.Role.Speaker -> {
speakersListExpanded += "$name ($address)\n"
}
Participant.Role.Listener -> {
participantsListExpanded += "$name ($address)\n"
allSpeaker = false
}
else -> { // For meetings created before 5.3 SDK
participantsListExpanded += "$name ($address)\n"
}
}
}
participantsListShort = participantsListShort.dropLast(2)
participantsListExpanded = participantsListExpanded.dropLast(1)
speakersListExpanded = speakersListExpanded.dropLast(1)
participantsShort.value = participantsListShort
participantsExpanded.value = participantsListExpanded
speakersExpanded.value = speakersListExpanded
// If all participants have Speaker role then it is a meeting, else it is a broadcast
isBroadcast.value = allSpeaker == false
Log.i(
"[Scheduled Conference] Conference [${subject.value}] is a ${if (allSpeaker) "meeting" else "broadcast"}"
)
}
}

View file

@ -53,6 +53,9 @@ class ConferenceWaitingRoomFragment : GenericFragment<ConferenceWaitingRoomFragm
val conferenceSubject = arguments?.getString("Subject")
viewModel.subject.value = conferenceSubject
val address = arguments?.getString("Address")
viewModel.findConferenceInfoByAddress(address)
viewModel.cancelConferenceJoiningEvent.observe(
viewLifecycleOwner
) {

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import java.util.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData
import org.linphone.activities.main.conference.data.Duration
@ -30,6 +31,7 @@ import org.linphone.activities.main.conference.data.TimeZoneData
import org.linphone.contact.ContactsSelectionViewModel
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
@ -41,6 +43,10 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
val scheduleForLater = MutableLiveData<Boolean>()
val isUpdate = MutableLiveData<Boolean>()
val isBroadcastAllowed = MutableLiveData<Boolean>()
val mode = MutableLiveData<String>()
val modesList: List<String>
val formattedDate = MutableLiveData<String>()
val formattedTime = MutableLiveData<String>()
@ -50,6 +56,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
val sendInviteViaEmail = MutableLiveData<Boolean>()
val participantsData = MutableLiveData<List<ConferenceSchedulingParticipantData>>()
val speakersData = MutableLiveData<List<ConferenceSchedulingParticipantData>>()
val address = MutableLiveData<Address>()
@ -74,6 +81,8 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
private var confInfo: ConferenceInfo? = null
private val conferenceScheduler = coreContext.core.createConferenceScheduler()
private val selectedSpeakersAddresses = MutableLiveData<ArrayList<Address>>()
private val listener = object : ConferenceSchedulerListenerStub() {
override fun onStateChanged(
conferenceScheduler: ConferenceScheduler,
@ -154,6 +163,13 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
scheduleForLater.value = false
isUpdate.value = false
isBroadcastAllowed.value = !corePreferences.disableBroadcastConference
modesList = arrayListOf(
AppUtils.getString(R.string.conference_schedule_mode_meeting),
AppUtils.getString(R.string.conference_schedule_mode_broadcast)
)
mode.value = modesList.first() // Meeting by default
isEncrypted.value = false
sendInviteViaChat.value = true
sendInviteViaEmail.value = false
@ -185,6 +201,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
override fun onCleared() {
conferenceScheduler.removeListener(listener)
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
super.onCleared()
}
@ -195,6 +212,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
}
fun populateFromConferenceInfo(conferenceInfo: ConferenceInfo) {
// Pre-set data from existing conference info, used when editing an already scheduled broadcast or meeting
confInfo = conferenceInfo
address.value = conferenceInfo.uri
@ -213,10 +231,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
scheduleForLater.value = conferenceDuration > 0
val participantsList = arrayListOf<Address>()
for (participant in conferenceInfo.participants) {
val speakersList = arrayListOf<Address>()
for (info in conferenceInfo.participantInfos) {
val participant = info.address
participantsList.add(participant)
if (info.role == Participant.Role.Speaker) {
speakersList.add(participant)
}
}
if (participantsList.count() == speakersList.count()) {
// All participants are speaker, this is a meeting, clear speakers
Log.i("[Conference Creation] Conference info is a meeting")
speakersList.clear()
mode.value = modesList.first()
} else {
Log.i("[Conference Creation] Conference info is a broadcast")
mode.value = modesList.last()
}
selectedAddresses.value = participantsList
selectedSpeakersAddresses.value = speakersList
computeParticipantsData()
}
@ -241,14 +275,57 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
fun computeParticipantsData() {
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
val list = arrayListOf<ConferenceSchedulingParticipantData>()
speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
val participantsList = arrayListOf<ConferenceSchedulingParticipantData>()
val speakersList = arrayListOf<ConferenceSchedulingParticipantData>()
for (address in selectedAddresses.value.orEmpty()) {
val data = ConferenceSchedulingParticipantData(address, isEncrypted.value == true)
list.add(data)
val isSpeaker = address in selectedSpeakersAddresses.value.orEmpty()
val data = ConferenceSchedulingParticipantData(
address,
showLimeBadge = isEncrypted.value == true,
showBroadcastControls = isModeBroadcastCurrentlySelected(),
speaker = isSpeaker,
onAddedToSpeakers = { data ->
Log.i(
"[Conference Creation] Participant [${address.asStringUriOnly()}] added to speakers"
)
val participants = arrayListOf<ConferenceSchedulingParticipantData>()
participants.addAll(participantsData.value.orEmpty())
participants.remove(data)
participantsData.value = participants
val speakers = arrayListOf<ConferenceSchedulingParticipantData>()
speakers.addAll(speakersData.value.orEmpty())
speakers.add(data)
speakersData.value = speakers
},
onRemovedFromSpeakers = { data ->
Log.i(
"[Conference Creation] Participant [${address.asStringUriOnly()}] removed from speakers"
)
val speakers = arrayListOf<ConferenceSchedulingParticipantData>()
speakers.addAll(speakersData.value.orEmpty())
speakers.remove(data)
speakersData.value = speakers
val participants = arrayListOf<ConferenceSchedulingParticipantData>()
participants.addAll(participantsData.value.orEmpty())
participants.add(data)
participantsData.value = participants
}
)
if (isSpeaker) {
speakersList.add(data)
} else {
participantsList.add(data)
}
}
participantsData.value = list
participantsData.value = participantsList
speakersData.value = speakersList
}
fun createConference() {
@ -260,8 +337,6 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
conferenceCreationInProgress.value = true
val core = coreContext.core
val participants = arrayOfNulls<Address>(selectedAddresses.value.orEmpty().size)
selectedAddresses.value?.toArray(participants)
val localAccount = core.defaultAccount
val localAddress = localAccount?.params?.identityAddress
@ -273,7 +348,25 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
conferenceInfo.organizer = localAddress
conferenceInfo.subject = subject.value
conferenceInfo.description = description.value
conferenceInfo.setParticipants(participants)
val participants = arrayOfNulls<ParticipantInfo>(selectedAddresses.value.orEmpty().size)
var index = 0
val isBroadcast = isModeBroadcastCurrentlySelected()
for (participant in participantsData.value.orEmpty()) {
val info = Factory.instance().createParticipantInfo(participant.sipAddress)
// For meetings, all participants must have Speaker role
info?.role = if (isBroadcast) Participant.Role.Listener else Participant.Role.Speaker
participants[index] = info
index += 1
}
for (speaker in speakersData.value.orEmpty()) {
val info = Factory.instance().createParticipantInfo(speaker.sipAddress)
info?.role = Participant.Role.Speaker
participants[index] = info
index += 1
}
conferenceInfo.setParticipantInfos(participants)
if (scheduleForLater.value == true) {
val startTime = getConferenceStartTimestamp()
conferenceInfo.dateTime = startTime
@ -287,6 +380,10 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
conferenceScheduler.info = conferenceInfo
}
fun isModeBroadcastCurrentlySelected(): Boolean {
return mode.value == AppUtils.getString(R.string.conference_schedule_mode_broadcast)
}
private fun computeTimeZonesList(): List<TimeZoneData> {
return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted()
}

View file

@ -61,6 +61,8 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
val networkReachable = MutableLiveData<Boolean>()
val isConferenceBroadcastWithListenerRole = MutableLiveData<Boolean>()
val askPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
@ -198,7 +200,10 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
}
layoutMenuSelected.value = false
updateLayout()
when (core.defaultConferenceLayout) {
Conference.Layout.Grid -> setMosaicLayout()
else -> setActiveSpeakerLayout()
}
if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
setBluetoothAudioRoute()
@ -216,6 +221,44 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
super.onCleared()
}
fun findConferenceInfoByAddress(stringAddress: String?) {
if (stringAddress != null) {
val address = Factory.instance().createAddress(stringAddress)
if (address != null) {
val conferenceInfo = coreContext.core.findConferenceInformationFromUri(address)
if (conferenceInfo != null) {
val myself = conferenceInfo.participantInfos.find {
it.address.asStringUriOnly() == coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
}
if (myself != null) {
Log.i(
"[Conference Waiting Room] Found our participant, it's role is [${myself.role}]"
)
val areWeListener = myself.role == Participant.Role.Listener
isConferenceBroadcastWithListenerRole.value = areWeListener
if (areWeListener) {
callParams.isVideoEnabled = false
callParams.videoDirection = MediaDirection.Inactive
updateVideoState()
updateLayout()
}
} else {
Log.e(
"[Conference Waiting Room] Failed to find ourselves in participants info"
)
}
} else {
Log.e(
"[Conference Waiting Room] Failed to find conference info using address [$stringAddress]"
)
}
}
} else {
Log.e("[Conference Waiting Room] Can't find conference info using null address!")
}
}
fun cancel() {
cancelConferenceJoiningEvent.value = Event(true)
}

View file

@ -35,8 +35,16 @@ class ConferencesSettingsViewModel : GenericSettingsViewModel() {
val layoutLabels = MutableLiveData<ArrayList<String>>()
private val layoutValues = arrayListOf<Int>()
val enableBroadcastListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.disableBroadcastConference = !newValue
}
}
val enableBroadcast = MutableLiveData<Boolean>()
init {
initLayoutsList()
enableBroadcast.value = !prefs.disableBroadcastConference
}
private fun initLayoutsList() {

View file

@ -34,10 +34,13 @@ class ConferenceParticipantData(
val isAdmin = MutableLiveData<Boolean>()
val isMeAdmin = MutableLiveData<Boolean>()
val isSpeaker = MutableLiveData<Boolean>()
init {
isAdmin.value = participant.isAdmin
isMeAdmin.value = conference.me.isAdmin
isSpeaker.value = participant.role == Participant.Role.Speaker
Log.i(
"[Conference Participant] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}"
)

View file

@ -56,9 +56,13 @@ class ConferenceViewModel : ViewModel() {
val twoOrMoreParticipants = MutableLiveData<Boolean>()
val moreThanTwoParticipants = MutableLiveData<Boolean>()
val speakingParticipantFound = MutableLiveData<Boolean>()
val speakingParticipant = MutableLiveData<ConferenceParticipantDeviceData>()
val meParticipant = MutableLiveData<ConferenceParticipantDeviceData>()
val isBroadcast = MutableLiveData<Boolean>()
val isMeListenerOnly = MutableLiveData<Boolean>()
val participantAdminStatusChangedEvent: MutableLiveData<Event<ConferenceParticipantData>> by lazy {
MutableLiveData<Event<ConferenceParticipantData>>()
}
@ -207,6 +211,7 @@ class ConferenceViewModel : ViewModel() {
speakingParticipant.value?.isActiveSpeaker?.value = false
device.isActiveSpeaker.value = true
speakingParticipant.value = device!!
speakingParticipantFound.value = true
} else if (device == null) {
Log.w(
"[Conference] Participant device [${participantDevice.address.asStringUriOnly()}] is the active speaker but couldn't find it in devices list"
@ -528,6 +533,23 @@ class ConferenceViewModel : ViewModel() {
val activelySpeakingParticipantDevice = conference.activeSpeakerParticipantDevice
var foundActivelySpeakingParticipantDevice = false
speakingParticipantFound.value = false
val conferenceInfo = conference.core.findConferenceInformationFromUri(
conference.conferenceAddress
)
var allSpeaker = true
for (info in conferenceInfo?.participantInfos.orEmpty()) {
if (info.role == Participant.Role.Listener) {
allSpeaker = false
}
}
isBroadcast.value = !allSpeaker
if (!allSpeaker) {
Log.i(
"[Conference] Not all participants are speaker, considering it is a broadcast"
)
}
for (participant in participantsList) {
val participantDevices = participant.devices
@ -539,6 +561,18 @@ class ConferenceViewModel : ViewModel() {
Log.i(
"[Conference] Participant device found: ${device.name} (${device.address.asStringUriOnly()})"
)
val info = conferenceInfo?.participantInfos?.find {
it.address.weakEqual(participant.address)
}
if (info != null) {
Log.i("[Conference] Participant role is [${info.role.name}]")
val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown
if (listener) {
continue
}
}
val deviceData = ConferenceParticipantDeviceData(device, false)
devices.add(deviceData)
@ -549,6 +583,7 @@ class ConferenceViewModel : ViewModel() {
speakingParticipant.value = deviceData
deviceData.isActiveSpeaker.value = true
foundActivelySpeakingParticipantDevice = true
speakingParticipantFound.value = true
}
}
}
@ -560,12 +595,26 @@ class ConferenceViewModel : ViewModel() {
val deviceData = devices.first()
speakingParticipant.value = deviceData
deviceData.isActiveSpeaker.value = true
speakingParticipantFound.value = false
}
for (device in conference.me.devices) {
Log.i(
"[Conference] Participant device for myself found: ${device.name} (${device.address.asStringUriOnly()})"
)
val info = conferenceInfo?.participantInfos?.find {
it.address.weakEqual(device.address)
}
if (info != null) {
Log.i("[Conference] Me role is [${info.role.name}]")
val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown
isMeListenerOnly.value = listener
if (listener) {
continue
}
}
val deviceData = ConferenceParticipantDeviceData(device, true)
devices.add(deviceData)
meParticipant.value = deviceData
@ -601,6 +650,7 @@ class ConferenceViewModel : ViewModel() {
if (speakingParticipant.value == null) {
speakingParticipant.value = deviceData
deviceData.isActiveSpeaker.value = true
speakingParticipantFound.value = false
}
conferenceParticipantDevices.value = sortedDevices
@ -641,6 +691,7 @@ class ConferenceViewModel : ViewModel() {
val deviceData = devices[1]
speakingParticipant.value = deviceData
deviceData.isActiveSpeaker.value = true
speakingParticipantFound.value = false
}
conferenceParticipantDevices.value = devices

View file

@ -377,6 +377,12 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "enter_video_conference_enable_full_screen_mode", value)
}
var disableBroadcastConference: Boolean
get() = config.getBool("app", "disable_broadcast_conference_feature", true)
set(value) {
config.setBool("app", "disable_broadcast_conference_feature", value)
}
/* Assistant */
var firstStart: Boolean

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
android:drawable="@drawable/shape_rect_orange_disabled_button" />
<item android:state_pressed="true"
android:drawable="@drawable/shape_rect_green_button" />
<item
android:drawable="@drawable/shape_rect_orange_button" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="5dp" />
<size android:height="20dp" android:width="20dp"/>
<solid android:color="@color/primary_color_disabled"/>
</shape>

View file

@ -166,6 +166,7 @@
android:id="@+id/active_speaker_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/miniatures"
@ -179,6 +180,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}"
coilVoipContact="@{conferenceViewModel.speakingParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
@ -196,7 +198,7 @@
android:background="@drawable/shape_button_background"
android:contentDescription="@string/content_description_participant_is_paused"
android:src="@drawable/icon_pause"
android:visibility="@{conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}"
android:visibility="@{conferenceViewModel.speakingParticipantFound &amp;&amp; conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
app:layout_constraintEnd_toEndOf="@id/active_speaker_background"
@ -253,6 +255,7 @@
android:id="@+id/local_participant_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -267,6 +270,7 @@
android:layout_height="0dp"
android:layout_margin="10dp"
android:contentDescription="@null"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
coilSelfAvatar="@{conferenceViewModel.meParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintDimensionRatio="1:1"
@ -297,6 +301,7 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
android:text="@{conferenceViewModel.meParticipant.contact.name ?? conferenceViewModel.meParticipant.displayName}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"

View file

@ -30,7 +30,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/conference_invite_title_font"
android:text="@string/conference_invite_title"
android:text="@{data.isBroadcast ? @string/conference_broadcast_invite_title : @string/conference_invite_title, default=@string/conference_invite_title}"
android:visibility="@{data.isConferenceUpdated || data.isConferenceCancelled ? View.GONE : View.VISIBLE}"/>
<TextView
@ -67,6 +67,15 @@
android:text="@{data.conferenceParticipantCount, default=`3 participants`}"
app:drawableStartCompat="@drawable/icon_schedule_participants"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginBottom="2dp"
android:visibility="@{data.isBroadcast &amp;&amp; data.isSpeaker ? View.VISIBLE : View.GONE}"
style="@style/conference_invite_dat_time_font"
android:text="@string/conference_broadcast_invite_you_are_speaker"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -109,7 +109,7 @@
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
@ -118,15 +118,32 @@
android:visibility="@{data.isConferenceCancelled ? View.VISIBLE : View.GONE, default=gone}"
android:text="@{data.canEdit ? @string/conference_scheduled_cancelled_by_me : @string/conference_scheduled_cancelled_by_organizer, default=@string/conference_scheduled_cancelled_by_organizer}"/>
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
style="@style/conference_invite_subject_font"
android:maxLines="2"
android:text="@{data.subject, default=`Lorem Ipsum`}"/>
android:orientation="horizontal">
<TextView
android:id="@+id/meeting_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
style="@style/conference_invite_subject_font"
android:text="@{data.isBroadcast() ? @string/conference_broadcast_title : @string/conference_meeting_title, default=@string/conference_meeting_title}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
style="@style/conference_invite_subject_font"
android:maxLines="2"
android:text="@{data.subject, default=`Lorem Ipsum`}"/>
</LinearLayout>
<LinearLayout
android:visibility="@{data.expanded ? View.GONE : View.VISIBLE}"
@ -182,24 +199,61 @@
android:contentDescription="@string/content_description_toggle_conference_info_details"
android:src="@drawable/button_conference_info" />
<ImageView
android:id="@+id/participants_icon"
<TextView
style="@style/conference_invite_desc_font"
android:id="@+id/speakers_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:contentDescription="@string/content_description_conference_participants"
android:text="@string/conference_invite_speakers_list_title"
android:textSize="16sp"
android:textColor="@color/voip_dark_gray"
android:drawableStart="@drawable/icon_schedule_participants"
android:drawablePadding="5dp"
android:visibility="@{data.isConferenceCancelled || !data.isBroadcast ? View.GONE : View.VISIBLE}"
android:src="@drawable/icon_schedule_participants"/>
<TextView
android:id="@+id/speakers_list"
style="@style/conference_invite_desc_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:layout_toStartOf="@id/selected_info"
android:layout_below="@id/speakers_title"
android:background="@color/transparent_color"
android:drawablePadding="5dp"
android:visibility="@{data.isConferenceCancelled || !data.isBroadcast ? View.GONE : View.VISIBLE}"
android:text="@{data.speakersExpanded, default=`Daryl Martin\nMartin Pecheur`}"
tools:ignore="LabelFor" />
<TextView
style="@style/conference_invite_desc_font"
android:id="@+id/participants_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/speakers_list"
android:layout_alignParentStart="true"
android:text="@string/conference_invite_participants_list_title"
android:textSize="16sp"
android:textColor="@color/voip_dark_gray"
android:drawableStart="@drawable/icon_schedule_participants"
android:drawablePadding="5dp"
android:visibility="@{data.isConferenceCancelled ? View.GONE : View.VISIBLE}"
android:src="@drawable/icon_schedule_participants"/>
<TextView
android:id="@+id/participants_list"
style="@style/conference_invite_desc_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginStart="23dp"
android:layout_marginEnd="10dp"
android:layout_toStartOf="@id/selected_info"
android:layout_toEndOf="@id/participants_icon"
android:layout_below="@id/participants_title"
android:background="@color/transparent_color"
android:drawablePadding="5dp"
android:visibility="@{data.isConferenceCancelled ? View.GONE : View.VISIBLE}"
@ -241,7 +295,7 @@
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:visibility="@{data.isConferenceCancelled ? View.GONE : View.VISIBLE}"
android:text="@string/conference_schedule_address_title"/>
android:text="@{data.isBroadcast ? @string/conference_schedule_broadcast_address_title : @string/conference_schedule_address_title, default=@string/conference_schedule_address_title}"/>
<LinearLayout
android:layout_width="match_parent"

View file

@ -137,6 +137,31 @@
android:layout_margin="10dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical"
android:visibility="@{viewModel.isBroadcastAllowed ? View.VISIBLE : View.GONE, default=gone}">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/conference_scheduling_font"
android:text="@string/conference_schedule_mode"/>
<Spinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_marginTop="5dp"
style="@style/conference_scheduling_font"
android:background="@drawable/icon_spinner_background"
app:entries="@{viewModel.modesList}"
app:selectedValue="@={viewModel.mode}"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -11,7 +11,8 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/conference_scheduling_participant_cell_height"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:background="@color/white_color">
<LinearLayout
android:layout_width="match_parent"
@ -76,6 +77,17 @@
</LinearLayout>
<ImageView
android:onClick="@{() -> data.changeIsSpeaker()}"
android:visibility="@{data.showBroadcastControls ? View.VISIBLE : View.GONE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:src="@{data.isSpeaker ? @drawable/field_remove : @drawable/field_add, default=@drawable/field_add}"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="5dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"

View file

@ -43,7 +43,7 @@
android:gravity="center"
android:orientation="horizontal"
style="@style/calls_list_header_font"
android:text="@{viewModel.scheduleForLater ? @string/conference_schedule_summary : @string/conference_group_call_summary, default=@string/conference_group_call_summary}"/>
android:text="@{viewModel.isModeBroadcastCurrentlySelected() ? @string/conference_schedule_broadcast_summary : viewModel.scheduleForLater ? @string/conference_schedule_summary : @string/conference_group_call_summary, default=@string/conference_group_call_summary}"/>
<View
android:layout_width="0dp"
@ -268,14 +268,49 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginBottom="20dp"
style="@style/conference_scheduling_font"
android:visibility="@{viewModel.scheduleForLater &amp;&amp; viewModel.sendInviteViaChat ? View.VISIBLE : View.GONE}"
android:text="@string/conference_schedule_send_invite_chat_summary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{viewModel.isModeBroadcastCurrentlySelected() ? View.VISIBLE : View.GONE}"
android:background="?attr/voipFormDisabledFieldBackgroundColor">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_marginLeft="20dp"
style="@style/calls_list_header_font"
android:text="@string/conference_schedule_speakers_list"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="@{viewModel.isModeBroadcastCurrentlySelected() ? View.VISIBLE : View.GONE}"
app:entries="@{viewModel.speakersData}"
app:layout="@{@layout/conference_scheduling_participant_cell}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_marginLeft="10dp"
style="@style/calls_list_header_font"
android:textColor="@color/light_grey_color"
android:visibility="@{viewModel.isModeBroadcastCurrentlySelected() &amp;&amp; viewModel.speakersData.size() == 0 ? View.VISIBLE : View.GONE}"
android:text="@string/conference_schedule_speakers_list_empty"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/voipFormDisabledFieldBackgroundColor">
<TextView
@ -288,6 +323,16 @@
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginLeft="10dp"
style="@style/calls_list_header_font"
android:textColor="@color/light_grey_color"
android:visibility="@{viewModel.participantsData.size() == 0 ? View.VISIBLE : View.GONE, default=gone}"
android:text="@string/conference_schedule_participants_list_empty"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -310,10 +355,28 @@
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_gravity="center"
android:background="@drawable/shape_rect_orange_button"
android:background="@drawable/rect_orange_button"
android:visibility="@{viewModel.isModeBroadcastCurrentlySelected() ? View.GONE : View.VISIBLE}"
android:text="@{viewModel.isUpdate ? @string/conference_schedule_edit : viewModel.scheduleForLater ? @string/conference_schedule_start : @string/conference_group_call_create, default=@string/conference_group_call_create}"
style="@style/big_orange_button_font"/>
<TextView
android:onClick="@{() -> viewModel.createConference()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="20dp"
android:paddingTop="8dp"
android:paddingRight="20dp"
android:paddingBottom="8dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_gravity="center"
android:background="@drawable/rect_orange_button"
android:enabled="@{viewModel.participantsData.size() != 0 &amp;&amp; viewModel.speakersData.size() != 0}"
android:visibility="@{viewModel.isModeBroadcastCurrentlySelected() ? View.VISIBLE : View.GONE, default=gone}"
android:text="@{viewModel.isUpdate ? @string/conference_broadcast_edit : @string/conference_broadcast_start, default=@string/conference_broadcast_start}"
style="@style/big_orange_button_font"/>
</LinearLayout>
<include

View file

@ -34,7 +34,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/conference_waiting_room_video_disabled"
android:text="@{viewModel.isConferenceBroadcastWithListenerRole ? @string/conference_waiting_room_listener_broadcast : @string/conference_waiting_room_video_disabled, default=@string/conference_waiting_room_video_disabled}"
android:visibility="@{viewModel.isVideoEnabled ? View.GONE : View.VISIBLE}" />
<androidx.constraintlayout.widget.ConstraintLayout
@ -234,7 +234,7 @@
android:layout_marginEnd="5dp"
android:background="@drawable/button_background_reverse"
android:contentDescription="@{viewModel.isMicrophoneMuted ? @string/content_description_disable_mic_mute : @string/content_description_enable_mic_mute}"
android:enabled="@{!viewModel.joinInProgress}"
android:enabled="@{!viewModel.isConferenceBroadcastWithListenerRole &amp;&amp; !viewModel.joinInProgress}"
android:onClick="@{() -> viewModel.toggleMuteMicrophone()}"
android:padding="5dp"
android:selected="@{viewModel.isMicrophoneMuted}"
@ -284,7 +284,7 @@
android:layout_marginStart="5dp"
android:background="@drawable/button_background_reverse"
android:contentDescription="@{viewModel.isVideoEnabled ? @string/content_description_disable_video : @string/content_description_enable_video}"
android:enabled="@{!viewModel.joinInProgress &amp;&amp; viewModel.isVideoAvailable &amp;&amp; !viewModel.isLowBandwidth}"
android:enabled="@{!viewModel.isConferenceBroadcastWithListenerRole &amp;&amp; !viewModel.joinInProgress &amp;&amp; viewModel.isVideoAvailable &amp;&amp; !viewModel.isLowBandwidth}"
android:onClick="@{() -> viewModel.toggleVideo()}"
android:padding="5dp"
android:selected="@{viewModel.isVideoEnabled}"

View file

@ -240,7 +240,6 @@
linphone:title="@{@string/account_setting_conference_factory_address}"
linphone:listener="@{viewModel.conferenceFactoryUriListener}"
linphone:defaultValue="@{viewModel.conferenceFactoryUri}"
android:visibility="gone"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
@ -248,7 +247,6 @@
linphone:title="@{@string/account_setting_audio_video_conference_factory_address}"
linphone:listener="@{viewModel.audioVideoConferenceFactoryUriListener}"
linphone:defaultValue="@{viewModel.audioVideoConferenceFactoryUri}"
android:visibility="gone"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
@ -256,7 +254,6 @@
linphone:title="@{@string/account_setting_end_to_end_encryption_keys_server_url}"
linphone:listener="@{viewModel.limeServerUrlListener}"
linphone:defaultValue="@{viewModel.limeServerUrl}"
android:visibility="gone"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include

View file

@ -72,6 +72,13 @@
linphone:selectedIndex="@{viewModel.layoutIndex}"
linphone:labels="@{viewModel.layoutLabels}"/>
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/conferences_settings_enable_broadcast_title}"
linphone:subtitle="@{@string/conferences_settings_enable_broadcast_summary}"
linphone:listener="@{viewModel.enableBroadcastListener}"
linphone:checked="@={viewModel.enableBroadcast}"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -46,10 +46,10 @@
android:layout_marginEnd="5dp"
android:background="@drawable/button_background_reverse"
android:contentDescription="@{callsViewModel.isMicrophoneMuted ? @string/content_description_disable_mic_mute : @string/content_description_enable_mic_mute}"
android:enabled="@{callsViewModel.isMuteMicrophoneEnabled}"
android:enabled="@{callsViewModel.isMuteMicrophoneEnabled &amp;&amp; !conferenceViewModel.isMeListenerOnly}"
android:onClick="@{() -> callsViewModel.toggleMuteMicrophone()}"
android:padding="5dp"
android:selected="@{callsViewModel.isMicrophoneMuted}"
android:selected="@{callsViewModel.isMicrophoneMuted || conferenceViewModel.isMeListenerOnly}"
android:src="@drawable/icon_toggle_mic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,1:1"
@ -96,10 +96,10 @@
android:layout_marginStart="5dp"
android:background="@drawable/button_background_reverse"
android:contentDescription="@{controlsViewModel.isVideoEnabled &amp;&amp; controlsViewModel.isSendingVideo ? @string/content_description_disable_video : @string/content_description_enable_video}"
android:enabled="@{controlsViewModel.isVideoAvailable &amp;&amp; !controlsViewModel.isVideoUpdateInProgress}"
android:enabled="@{controlsViewModel.isVideoAvailable &amp;&amp; !controlsViewModel.isVideoUpdateInProgress &amp;&amp; !conferenceViewModel.isMeListenerOnly}"
android:onClick="@{() -> (!conferenceViewModel.conferenceExists || conferenceViewModel.conferenceDisplayMode != ConferenceDisplayMode.AUDIO_ONLY) ? controlsViewModel.toggleVideo() : conferenceViewModel.switchLayoutFromAudioOnlyToActiveSpeaker()}"
android:padding="5dp"
android:selected="@{controlsViewModel.isVideoEnabled &amp;&amp; controlsViewModel.isSendingVideo}"
android:selected="@{controlsViewModel.isVideoEnabled &amp;&amp; controlsViewModel.isSendingVideo &amp;&amp; !conferenceViewModel.isMeListenerOnly}"
android:visibility="@{controlsViewModel.hideVideo ? View.GONE : View.VISIBLE}"
android:src="@drawable/icon_toggle_camera"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -39,6 +39,7 @@
android:gravity="center"
android:onClick="@{() -> controlsViewModel.showNumpad()}"
android:text="@string/call_action_numpad"
android:enabled="@{!conferenceViewModel.isBroadcast}"
app:drawableTopCompat="@drawable/icon_call_numpad"
app:layout_constraintBottom_toBottomOf="@id/chat"
app:layout_constraintEnd_toStartOf="@id/call_stats"
@ -175,6 +176,7 @@
android:gravity="center"
android:onClick="@{() -> controlsViewModel.goToCallsList()}"
android:text="@string/call_action_calls_list"
android:enabled="@{!conferenceViewModel.isBroadcast}"
app:drawableTopCompat="@drawable/icon_calls_list"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -125,6 +125,7 @@
android:id="@+id/active_speaker_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -139,6 +140,7 @@
android:layout_height="0dp"
android:layout_margin="10dp"
android:contentDescription="@null"
android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}"
coilVoipContact="@{conferenceViewModel.speakingParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
@ -157,7 +159,7 @@
android:background="@drawable/shape_button_background"
android:contentDescription="@string/content_description_participant_is_paused"
android:src="@drawable/icon_pause"
android:visibility="@{conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}"
android:visibility="@{conferenceViewModel.speakingParticipantFound &amp;&amp; conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
app:layout_constraintEnd_toEndOf="@id/active_speaker_background"
@ -254,6 +256,7 @@
android:id="@+id/local_participant_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -268,6 +271,7 @@
android:layout_height="0dp"
android:layout_margin="10dp"
android:contentDescription="@null"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
coilSelfAvatar="@{conferenceViewModel.meParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintDimensionRatio="1:1"
@ -298,6 +302,7 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}"
android:text="@{conferenceViewModel.meParticipant.contact.name ?? conferenceViewModel.meParticipant.displayName}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"

View file

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="data"
type="org.linphone.activities.voip.data.ConferenceParticipantData" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="5dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/avatar"
android:layout_width="@dimen/contact_avatar_size"
android:layout_height="@dimen/contact_avatar_size"
android:contentDescription="@null"
coilContact="@{data}"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_centerVertical="true"
android:background="@drawable/generated_avatar_bg"
android:src="@drawable/voip_single_contact_avatar"/>
<ImageView
android:id="@+id/delete"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerInParent="true"
android:contentDescription="@string/content_description_remove_contact_from_chat_room"
android:onClick="@{() -> data.removeParticipantFromConference()}"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:src="@drawable/icon_cancel_alt"
android:visibility="@{data.isMeAdmin ? View.VISIBLE : View.INVISIBLE}" />
<LinearLayout
android:id="@+id/speakerLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/delete"
android:layout_marginStart="5dp"
android:layout_marginEnd="10dp"
android:visibility="@{data.isSpeaker ? View.VISIBLE : View.INVISIBLE}">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:padding="5dp"
android:scaleType="centerInside"
android:contentDescription="@string/content_description_contact_is_speaker"
android:src="@drawable/check_selected" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/conference_participants_list_speaker_label"
android:textColor="?attr/primaryTextColor" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="10dp"
android:layout_toStartOf="@id/speakerLayout"
android:layout_toEndOf="@id/avatar"
android:orientation="vertical">
<org.linphone.views.MarqueeTextView
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@{data.contact.name ?? data.displayName, default=`Bilbo Baggins`}" />
<org.linphone.views.MarqueeTextView
android:id="@+id/sipUri"
style="@style/sip_uri_small_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:text="@{data.sipUri, default=`sip:bilbo.baggins@sip.linphone.org`}" />
</LinearLayout>
</RelativeLayout>
</layout>

View file

@ -76,7 +76,7 @@
android:layout_height="wrap_content"
android:orientation="vertical"
app:entries="@{conferenceViewModel.conferenceParticipants}"
app:layout="@{@layout/voip_conference_participant_cell}" />
app:layout="@{conferenceViewModel.isBroadcast ? @layout/voip_conference_participant_broadcast_cell : @layout/voip_conference_participant_cell}" />
</androidx.core.widget.NestedScrollView>

View file

@ -799,4 +799,25 @@
<string name="chat_message_many_reactions">%s réactions</string>
<string name="conference_invitation_notification_short_desc">invitation à une conférence</string>
<string name="chat_message_voice_recording_notification_short_desc">message vocal</string>
<string name="conference_schedule_mode">Mode</string>
<string name="conference_schedule_mode_meeting">Réunion</string>
<string name="conference_schedule_mode_broadcast">Webinar</string>
<string name="conference_schedule_broadcast_summary">Informations du webinar</string>
<string name="conference_schedule_speakers_list">Liste des intervenants</string>
<string name="conference_schedule_speakers_list_empty">Choisissez au moins un intervenant</string>
<string name="conference_schedule_participants_list_empty">Choisissez au moins un participant</string>
<string name="conference_broadcast_start">Planifier un webinar</string>
<string name="conference_broadcast_edit">Modifier le webinar</string>
<string name="conference_meeting_title">Réunion :</string>
<string name="conference_broadcast_title">Webinar :</string>
<string name="conference_invite_speakers_list_title">Intervenants</string>
<string name="conference_invite_participants_list_title">Participants</string>
<string name="conference_broadcast_invite_title">Invitation au webinar :</string>
<string name="conference_broadcast_invite_you_are_speaker">(vous êtes un intervenant)</string>
<string name="conference_schedule_broadcast_address_title">Adresse du webinar</string>
<string name="conference_waiting_room_listener_broadcast">Vous n\'êtes pas un intervenant de ce webinar</string>
<string name="conferences_settings_enable_broadcast_title">Autoriser les webinar</string>
<string name="conferences_settings_enable_broadcast_summary">Fonctionalité encore en béta !</string>
<string name="conference_participants_list_speaker_label">Intervenant</string>
<string name="content_description_contact_is_speaker">Contact is a speaker</string>
</resources>

View file

@ -6,6 +6,7 @@
<!-- You can use https://material.io/design/color/#tools-for-picking-colors to get dark color value from primary -->
<color name="primary_dark_color">#e65000</color>
<color name="primary_light_color">#ffab4d</color>
<color name="primary_color_disabled">#4DFE5E00</color> <!-- Try to have a color that renders well on both light & dark theme -->
<color name="notification_led_color">#ff8000</color>
<color name="black_color">#000000</color>

View file

@ -274,11 +274,15 @@
<string name="conference_schedule_subject_hint">Meeting subject</string>
<string name="conference_group_call_subject_hint">Group call subject</string>
<string name="conference_schedule_address_title">Meeting address</string>
<string name="conference_schedule_broadcast_address_title">Broadcast address</string>
<string name="conference_schedule_description_title">Add a description</string>
<string name="conference_schedule_description_hint">Description</string>
<string name="conference_schedule_date">Date</string>
<string name="conference_schedule_time">Time</string>
<string name="conference_schedule_duration">Duration</string>
<string name="conference_schedule_mode">Mode</string>
<string name="conference_schedule_mode_meeting">Meeting</string>
<string name="conference_schedule_mode_broadcast">Broadcast</string>
<string name="conference_schedule_timezone">Timezone</string>
<string name="conference_schedule_send_invite_chat">Send invite via &appName;</string>
<string name="conference_schedule_send_updated_invite_chat">Send update via &appName;</string>
@ -287,11 +291,17 @@
<string name="conference_schedule_encryption">Would you like to encrypt the meeting?</string>
<string name="conference_schedule_send_invite_chat_summary">Invite will be sent out from my &appName; account</string>
<string name="conference_schedule_participants_list">Participants list</string>
<string name="conference_schedule_speakers_list">Speakers list</string>
<string name="conference_schedule_speakers_list_empty">Select at least one speaker</string>
<string name="conference_schedule_participants_list_empty">Select at least one participant</string>
<string name="conference_organizer">Organizer</string>
<string name="conference_schedule_summary">Meeting info</string>
<string name="conference_group_call_summary">Group call info</string>
<string name="conference_schedule_broadcast_summary">Broadcast info</string>
<string name="conference_schedule_start">Schedule meeting</string>
<string name="conference_schedule_edit">Edit meeting</string>
<string name="conference_broadcast_start">Schedule broadcast</string>
<string name="conference_broadcast_edit">Edit broadcast</string>
<string name="conference_group_call_create">Start group call</string>
<string name="conference_schedule_address_copied_to_clipboard">Meeting address copied into clipboard</string>
<string name="conference_schedule_info_not_sent_to_participant">Failed to send meeting info to a participant</string>
@ -300,6 +310,8 @@
<string name="conference_default_title">Remote group call</string>
<string name="conference_local_title">Local group call</string>
<string name="conference_invite_title">Meeting invite:</string>
<string name="conference_broadcast_invite_title">Broadcast invite:</string>
<string name="conference_broadcast_invite_you_are_speaker">(you are a speaker)</string>
<string name="conference_update_title">Meeting has been updated:</string>
<string name="conference_cancel_title">Meeting has been cancelled:</string>
<string name="conference_description_title">Description</string>
@ -311,6 +323,7 @@
<string name="conference_waiting_room_start_call">Start</string>
<string name="conference_waiting_room_cancel_call">Cancel</string>
<string name="conference_waiting_room_video_disabled">Video is currently disabled</string>
<string name="conference_waiting_room_listener_broadcast">You aren\'t a speaker in this broadcast</string>
<string name="conference_scheduled">Meetings</string>
<string name="conference_layout_too_many_participants_for_mosaic">You can\'t change group call layout as there is too many participants</string>
<string name="conference_too_many_participants_for_mosaic_layout">There is too many participants for mosaic layout, switching to active speaker</string>
@ -346,6 +359,11 @@
</plurals>
<string name="conference_scheduled_cancelled_by_organizer">Conference has been cancelled by organizer</string>
<string name="conference_scheduled_cancelled_by_me">You have cancelled the conference</string>
<string name="conference_meeting_title">Meeting:</string>
<string name="conference_broadcast_title">Broadcast:</string>
<string name="conference_invite_speakers_list_title">Speakers</string>
<string name="conference_invite_participants_list_title">Participants</string>
<string name="conference_participants_list_speaker_label">Speaker</string>
<!-- Call -->
<string name="call_incoming_title">Incoming Call</string>
@ -746,6 +764,8 @@
<!-- Conferences settings -->
<string name="conferences_settings_layout_title">Default layout</string>
<string name="conferences_settings_enable_broadcast_title">Allow broadcasts</string>
<string name="conferences_settings_enable_broadcast_summary">Feature currently in beta!</string>
<!-- Notifications -->
<string name="notification_channel_service_id" translatable="false">linphone_notification_service_id</string>
@ -851,6 +871,7 @@
<string name="content_description_remove_contact_from_chat_room">Remove contact from conversation</string>
<string name="content_description_contact_is_admin">Contact is an admin in this conversation</string>
<string name="content_description_contact_is_not_admin">Contact isn\'t an admin in this conversation</string>
<string name="content_description_contact_is_speaker">Contact is a speaker</string>
<string name="content_description_ephemeral_chat_room">Messages are ephemeral in this conversation</string>
<string name="content_description_muted_chat_room">Notifications are disabled for this conversation</string>
<string name="content_description_contact_can_do_encryption">Contact can be invited in encrypted conversations</string>