Started meetings list

This commit is contained in:
Sylvain Berfini 2023-10-09 15:19:41 +02:00
parent 4a11554fa5
commit 3873028209
35 changed files with 1458 additions and 17 deletions

View file

@ -145,6 +145,16 @@ class MainActivity : AppCompatActivity() {
// TODO FIXME: uncomment
// startActivity(Intent(this, WelcomeActivity::class.java))
coreContext.greenToastToShowEvent.observe(this) {
it.consume { pair ->
val message = pair.first
val icon = pair.second
showGreenToast(message, icon)
}
}
}
override fun onStart() {
coreContext.postOnCoreThread {
val startDestination = when (corePreferences.defaultFragment) {
CONTACTS_FRAGMENT_ID -> {
@ -163,10 +173,10 @@ class MainActivity : AppCompatActivity() {
)
R.id.conversationsFragment
}
/*MEETINGS_FRAGMENT_ID -> {
MEETINGS_FRAGMENT_ID -> {
Log.i("$TAG Latest visited page is meetings, setting it as start destination")
R.id.meetingsFragment
}*/
}
else -> { // Default
Log.i("$TAG No latest visited page stored, using default one (call history)")
R.id.historyFragment
@ -179,13 +189,7 @@ class MainActivity : AppCompatActivity() {
}
}
coreContext.greenToastToShowEvent.observe(this) {
it.consume { pair ->
val message = pair.first
val icon = pair.second
showGreenToast(message, icon)
}
}
super.onStart()
}
override fun onPause() {
@ -199,9 +203,9 @@ class MainActivity : AppCompatActivity() {
R.id.conversationsFragment -> {
CHAT_FRAGMENT_ID
}
/*R.id.meetingsFragment -> {
R.id.meetingsFragment -> {
MEETINGS_FRAGMENT_ID
}*/
}
else -> { // Default
HISTORY_FRAGMENT_ID
}

View file

@ -65,6 +65,9 @@ class ConversationFragment : GenericFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// This fragment is displayed in a SlidingPane "child" area
isSlidingPaneChild = true
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()

View file

@ -139,6 +139,18 @@ class ConversationsFragment : GenericFragment() {
}
}
}
sharedViewModel.navigateToMeetingsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
// To prevent any previously seen conversation to show up when navigating back to here later
binding.chatNavContainer.findNavController().popBackStack()
val action = ConversationsFragmentDirections.actionConversationsFragmentToMeetingsFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {

View file

@ -24,7 +24,7 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactsManager
import org.linphone.core.CallLog
import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoom.Capabilities
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
@ -46,8 +46,22 @@ class ConversationsListViewModel @UiThread constructor() : AbstractTopBarViewMod
private var currentFilter = ""
private val coreListener = object : CoreListenerStub() {
override fun onCallLogUpdated(core: Core, callLog: CallLog) {
computeChatRoomsList(currentFilter)
@WorkerThread
override fun onChatRoomStateChanged(
core: Core,
chatRoom: ChatRoom,
state: ChatRoom.State?
) {
Log.i(
"$TAG Chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]"
)
when (state) {
ChatRoom.State.Created, ChatRoom.State.Instantiated, ChatRoom.State.Deleted -> {
computeChatRoomsList(currentFilter)
}
else -> {}
}
}
}

View file

@ -79,6 +79,9 @@ class ContactFragment : GenericFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// This fragment is displayed in a SlidingPane "child" area
isSlidingPaneChild = true
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()

View file

@ -142,6 +142,18 @@ class ContactsFragment : GenericFragment() {
}
}
}
sharedViewModel.navigateToMeetingsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
// To prevent any previously seen contact to show up when navigating back to here later
binding.contactsNavContainer.findNavController().popBackStack()
val action = ContactsFragmentDirections.actionContactsFragmentToMeetingsFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {

View file

@ -97,6 +97,9 @@ class EditContactFragment : GenericFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// This fragment is displayed in a SlidingPane "child" area
isSlidingPaneChild = true
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()

View file

@ -89,13 +89,16 @@ class BottomNavBarFragment : Fragment() {
}
binding.setOnMeetingsClicked {
// TODO: meeting feature
if (sharedViewModel.currentlyDisplayedFragment.value != R.id.meetingsFragment) {
sharedViewModel.navigateToMeetingsEvent.value = Event(true)
}
}
sharedViewModel.currentlyDisplayedFragment.observe(viewLifecycleOwner) {
viewModel.contactsSelected.value = it == R.id.contactsFragment
viewModel.callsSelected.value = it == R.id.historyFragment
viewModel.conversationsSelected.value = it == R.id.conversationsFragment
viewModel.meetingsSelected.value = it == R.id.meetingsFragment
}
sharedViewModel.resetMissedCallsCountEvent.observe(viewLifecycleOwner) {

View file

@ -26,7 +26,6 @@ import androidx.annotation.UiThread
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.main.viewmodel.SharedMainViewModel
@ -38,6 +37,8 @@ abstract class GenericFragment : Fragment() {
protected lateinit var sharedViewModel: SharedMainViewModel
protected var isSlidingPaneChild: Boolean = false
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
Log.d("$TAG ${getFragmentRealClassName()} handleOnBackPressed")
@ -114,7 +115,7 @@ abstract class GenericFragment : Fragment() {
// This allow to navigate a SlidingPane child nav graph.
// This only concerns fragments for which the nav graph is inside a SlidingPane layout.
// In our case it's all graphs except the main one.
if (findNavController().graph.id == R.id.main_nav_graph) return false
if (!isSlidingPaneChild) return false
val isSlidingPaneFlat = sharedViewModel.isSlidingPaneSlideable.value == false
Log.d(

View file

@ -75,6 +75,9 @@ class HistoryContactFragment : GenericFragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// This fragment is displayed in a SlidingPane "child" area
isSlidingPaneChild = true
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()

View file

@ -139,6 +139,18 @@ class HistoryFragment : GenericFragment() {
}
}
}
sharedViewModel.navigateToMeetingsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.historyFragment) {
// To prevent any previously seen call log to show up when navigating back to here later
binding.historyNavContainer.findNavController().popBackStack()
val action = HistoryFragmentDirections.actionHistoryFragmentToMeetingsFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {

View file

@ -0,0 +1,113 @@
package org.linphone.ui.main.meetings.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.MeetingListCellBinding
import org.linphone.databinding.MeetingsListDecorationBinding
import org.linphone.ui.main.meetings.model.MeetingModel
import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter
class MeetingsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<MeetingModel, RecyclerView.ViewHolder>(MeetingDiffCallback()), HeaderAdapter {
var selectedAdapterPosition = -1
val meetingClickedEvent: MutableLiveData<Event<MeetingModel>> by lazy {
MutableLiveData<Event<MeetingModel>>()
}
val meetingLongClickedEvent: MutableLiveData<Event<MeetingModel>> by lazy {
MutableLiveData<Event<MeetingModel>>()
}
override fun displayHeaderForPosition(position: Int): Boolean {
if (position == 0) return true
val previous = getItem(position - 1)
val item = getItem(position)
return previous.month != item.month
}
override fun getHeaderViewForPosition(context: Context, position: Int): View {
val binding = MeetingsListDecorationBinding.inflate(LayoutInflater.from(context))
val item = getItem(position)
binding.header.text = item.month
return binding.root
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: MeetingListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.meeting_list_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
fun resetSelection() {
notifyItemChanged(selectedAdapterPosition)
selectedAdapterPosition = -1
}
inner class ViewHolder(
val binding: MeetingListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(meetingModel: MeetingModel) {
with(binding) {
model = meetingModel
val hasPrevious = bindingAdapterPosition > 0
firstMeetingOfTheDay = if (hasPrevious) {
val previous = getItem(bindingAdapterPosition - 1)
previous.day != meetingModel.day || previous.dayNumber != meetingModel.dayNumber
} else {
true
}
lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
binding.setOnClickListener {
meetingClickedEvent.value = Event(meetingModel)
}
binding.setOnLongClickListener {
selectedAdapterPosition = bindingAdapterPosition
binding.root.isSelected = true
meetingLongClickedEvent.value = Event(meetingModel)
true
}
executePendingBindings()
}
}
}
private class MeetingDiffCallback : DiffUtil.ItemCallback<MeetingModel>() {
override fun areItemsTheSame(oldItem: MeetingModel, newItem: MeetingModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MeetingModel, newItem: MeetingModel): Boolean {
return oldItem.subject.value == newItem.subject.value && oldItem.time == newItem.time
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import org.linphone.databinding.MeetingFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.Event
class MeetingFragment : GenericFragment() {
companion object {
private const val TAG = "[Meeting Fragment]"
}
private lateinit var binding: MeetingFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MeetingFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack(): Boolean {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
// If not done, when going back to ConversationsFragment this fragment will be created again
return findNavController().popBackStack()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// This fragment is displayed in a SlidingPane "child" area
isSlidingPaneChild = true
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.view.doOnPreDraw
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.MeetingsFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.SlidingPaneBackPressedCallback
class MeetingsFragment : GenericFragment() {
companion object {
private const val TAG = "[Meetings Fragment]"
}
private lateinit var binding: MeetingsFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MeetingsFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.scheduleMeetingFragment) {
// Holds fragment in place while new contact fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.root.doOnPreDraw {
val slidingPane = binding.slidingPaneLayout
slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
sharedViewModel.isSlidingPaneSlideable.value = slidingPane.isSlideable
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
SlidingPaneBackPressedCallback(slidingPane)
)
}
sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner
) {
it.consume {
Log.i("$TAG Closing sliding pane")
binding.slidingPaneLayout.closePane()
}
}
sharedViewModel.openSlidingPaneEvent.observe(
viewLifecycleOwner
) {
it.consume {
Log.i("$TAG Opening sliding pane")
binding.slidingPaneLayout.openPane()
}
}
sharedViewModel.showScheduleMeetingEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Navigating to schedule meeting fragment")
findNavController().navigate(R.id.action_global_scheduleMeetingFragment)
}
}
sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.meetingsFragment) {
// To prevent any previously seen meeting to show up when navigating back to here later
binding.meetingsNavContainer.findNavController().popBackStack()
val action = MeetingsFragmentDirections.actionMeetingsFragmentToContactsFragment()
findNavController().navigate(action)
}
}
}
sharedViewModel.navigateToCallsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.meetingsFragment) {
// To prevent any previously seen meeting to show up when navigating back to here later
binding.meetingsNavContainer.findNavController().popBackStack()
val action = MeetingsFragmentDirections.actionMeetingsFragmentToHistoryFragment()
findNavController().navigate(action)
}
}
}
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.meetingsFragment) {
// To prevent any previously seen meeting to show up when navigating back to here later
binding.meetingsNavContainer.findNavController().popBackStack()
val action = MeetingsFragmentDirections.actionMeetingsFragmentToConversationsFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {
super.onResume()
sharedViewModel.currentlyDisplayedFragment.value = R.id.meetingsFragment
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.MeetingsListFragmentBinding
import org.linphone.ui.main.fragment.AbstractTopBarFragment
import org.linphone.ui.main.meetings.adapter.MeetingsListAdapter
import org.linphone.ui.main.meetings.viewmodel.MeetingsListViewModel
import org.linphone.utils.Event
import org.linphone.utils.RecyclerViewHeaderDecoration
import org.linphone.utils.hideKeyboard
import org.linphone.utils.showKeyboard
class MeetingsListFragment : AbstractTopBarFragment() {
companion object {
private const val TAG = "[Meetings List Fragment]"
}
private lateinit var binding: MeetingsListFragmentBinding
private lateinit var listViewModel: MeetingsListViewModel
private lateinit var adapter: MeetingsListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MeetingsListFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listViewModel = requireActivity().run {
ViewModelProvider(this)[MeetingsListViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
adapter = MeetingsListAdapter(viewLifecycleOwner)
binding.meetingsList.setHasFixedSize(true)
binding.meetingsList.adapter = adapter
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter, true)
binding.meetingsList.addItemDecoration(headerItemDecoration)
val layoutManager = LinearLayoutManager(requireContext())
binding.meetingsList.layoutManager = layoutManager
binding.setNewMeetingClicked {
sharedViewModel.showScheduleMeetingEvent.value = Event(true)
}
binding.setTodayClickListener {
val todayMeeting = listViewModel.meetings.value.orEmpty().find {
it.isToday
}
val position = if (todayMeeting != null) {
listViewModel.meetings.value.orEmpty().indexOf(todayMeeting)
} else {
0 // TODO FIXME: improve by getting closest meeting
}
binding.meetingsList.smoothScrollToPosition(position)
}
listViewModel.meetings.observe(viewLifecycleOwner) {
val currentCount = adapter.itemCount
adapter.submitList(it)
Log.i("$TAG Meetings list ready with [${it.size}] items")
if (currentCount < it.size) {
binding.meetingsList.scrollToPosition(0)
}
}
sharedViewModel.defaultAccountChangedEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i(
"$TAG Default account changed, updating avatar in top bar & re-computing meetings list"
)
}
}
// TopBarFragment related
setViewModelAndTitle(
listViewModel,
getString(R.string.bottom_navigation_meetings_label)
)
listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
listViewModel.applyFilter(filter.trim())
}
listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.topBar.search.showKeyboard()
} else {
binding.topBar.search.hideKeyboard()
}
}
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import org.linphone.databinding.MeetingScheduleFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.Event
class ScheduleMeetingFragment : GenericFragment() {
companion object {
private const val TAG = "[Schedule Meeting Fragment]"
}
private lateinit var binding: MeetingScheduleFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MeetingScheduleFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack(): Boolean {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
// If not done, when going back to ConversationsFragment this fragment will be created again
return findNavController().popBackStack()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.model
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.ConferenceInfo
import org.linphone.core.Participant
import org.linphone.utils.TimestampUtils
class MeetingModel @WorkerThread constructor(conferenceInfo: ConferenceInfo) {
val id = conferenceInfo.uri?.asStringUriOnly()
private val timestamp = conferenceInfo.dateTime
val day = TimestampUtils.dayOfWeek(timestamp)
val dayNumber = TimestampUtils.dayOfMonth(timestamp)
val month = TimestampUtils.month(timestamp)
val isToday = TimestampUtils.isToday(timestamp)
private val startTime = TimestampUtils.timeToString(timestamp)
private val endTime = TimestampUtils.timeToString(timestamp + (conferenceInfo.duration * 60))
val time = "$startTime - $endTime"
val isBroadcast = MutableLiveData<Boolean>()
val subject = MutableLiveData<String>()
init {
subject.postValue(conferenceInfo.subject)
var allSpeaker = true
for (participant in conferenceInfo.participantInfos) {
if (participant.role == Participant.Role.Listener) {
allSpeaker = false
break
}
}
isBroadcast.postValue(!allSpeaker)
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.meetings.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ConferenceInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.meetings.model.MeetingModel
import org.linphone.ui.main.viewmodel.AbstractTopBarViewModel
class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() {
companion object {
private const val TAG = "[Meetings List ViewModel]"
}
val meetings = MutableLiveData<ArrayList<MeetingModel>>()
val fetchInProgress = MutableLiveData<Boolean>()
private var currentFilter = ""
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) {
Log.i("$TAG Conference info received [${conferenceInfo.uri?.asStringUriOnly()}]")
computeMeetingsList(currentFilter)
}
}
init {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
computeMeetingsList(currentFilter)
}
}
@UiThread
override fun onCleared() {
super.onCleared()
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
}
}
@UiThread
fun applyFilter(filter: String = currentFilter) {
currentFilter = filter
coreContext.postOnCoreThread {
computeMeetingsList(filter)
}
}
@WorkerThread
private fun computeMeetingsList(filter: String) {
val list = arrayListOf<MeetingModel>()
// TODO FIXME: get list from default account
for (conferenceInfo in coreContext.core.conferenceInformationList) {
val model = MeetingModel(conferenceInfo)
// TODO FIXME: apply filter
list.add(model)
}
meetings.postValue(list)
}
}

View file

@ -49,6 +49,10 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val navigateToMeetingsEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
var currentlyDisplayedFragment = MutableLiveData<Int>()
/* Top bar related */
@ -104,4 +108,10 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
val showConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()
}
/* Meetings related */
val showScheduleMeetingEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
}

View file

@ -23,11 +23,14 @@ import androidx.annotation.AnyThread
import java.text.DateFormat
import java.text.Format
import java.text.SimpleDateFormat
import java.time.format.TextStyle
import java.util.*
import org.linphone.LinphoneApplication.Companion.coreContext
class TimestampUtils {
companion object {
private const val TAG = "[Timestamp Utils]"
@AnyThread
fun isToday(timestamp: Long, timestampInSecs: Boolean = true): Boolean {
val cal = Calendar.getInstance()
@ -60,6 +63,48 @@ class TimestampUtils {
return dateFormatter.format(calendar.time)
}
@AnyThread
fun dayOfWeek(timestamp: Long, timestampInSecs: Boolean = true): String {
val calendar = Calendar.getInstance()
calendar.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
val dayName = calendar.getDisplayName(
Calendar.DAY_OF_WEEK,
TextStyle.SHORT.ordinal,
Locale.getDefault()
)
val upperCased = dayName?.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.getDefault()
)
} else {
it.toString()
}
} ?: "?"
val shorten = upperCased.substring(0, 3)
return "$shorten."
}
@AnyThread
fun dayOfMonth(timestamp: Long, timestampInSecs: Boolean = true): String {
val calendar = Calendar.getInstance()
calendar.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
return calendar.get(Calendar.DAY_OF_MONTH).toString()
}
@AnyThread
fun month(timestamp: Long, timestampInSecs: Boolean = true): String {
val calendar = Calendar.getInstance()
calendar.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp
return calendar.getDisplayName(
Calendar.MONTH,
TextStyle.SHORT.ordinal,
Locale.getDefault()
)
?.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
?: "?"
}
@AnyThread
fun timeToString(time: Long, timestampInSecs: Boolean = true): String {
val use24hFormat = android.text.format.DateFormat.is24HourFormat(

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M208,32L184,32L184,24a8,8 0,0 0,-16 0v8L88,32L88,24a8,8 0,0 0,-16 0v8L48,32A16,16 0,0 0,32 48L32,208a16,16 0,0 0,16 16L208,224a16,16 0,0 0,16 -16L224,48A16,16 0,0 0,208 32ZM72,48v8a8,8 0,0 0,16 0L88,48h80v8a8,8 0,0 0,16 0L184,48h24L208,80L48,80L48,48ZM208,208L48,208L48,96L208,96L208,208ZM112,120v64a8,8 0,0 1,-16 0L96,132.94l-4.42,2.22a8,8 0,0 1,-7.16 -14.32l16,-8A8,8 0,0 1,112 120ZM171.16,150.45L152,176h16a8,8 0,0 1,0 16L136,192a8,8 0,0 1,-6.4 -12.8l28.78,-38.37A8,8 0,1 0,145.07 132a8,8 0,1 1,-13.85 -8A24,24 0,0 1,176 136,23.76 23.76,0 0,1 171.16,150.45Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<size android:width="60dp" android:height="60dp" />
<solid android:color="@color/orange_main_500"/>
</shape>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M192,48L64,48A16,16 0,0 0,48 64L48,192a16,16 0,0 0,16 16L192,208a16,16 0,0 0,16 -16L208,64A16,16 0,0 0,192 48ZM192,192L64,192L64,64L192,64L192,192ZM240,56L240,200a8,8 0,0 1,-16 0L224,56a8,8 0,0 1,16 0ZM32,56L32,200a8,8 0,0 1,-16 0L16,56a8,8 0,0 1,16 0Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,32 @@
<?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" />
</data>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/meetings_list"
android:name="org.linphone.ui.main.meetings.fragment.MeetingsListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_with_nav_width"
android:layout_height="match_parent"
app:layout="@layout/meetings_list_fragment"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/meetings_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/meetings_nav_graph"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
</layout>

View file

@ -0,0 +1,137 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="newMeetingClicked"
type="View.OnClickListener" />
<variable
name="filterClickListener"
type="View.OnClickListener" />
<variable
name="todayClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.meetings.viewmodel.MeetingsListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/orange_main_500">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_meeting_image, no_meeting_label"
android:visibility="@{viewModel.meetings.empty ? View.VISIBLE : View.GONE}" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/bottom_nav_bar"
android:name="org.linphone.ui.main.fragment.BottomNavBarFragment"
android:layout_width="75dp"
android:layout_height="0dp"
bind:layout="@layout/bottom_nav_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<include
android:id="@+id/top_bar"
layout="@layout/top_bar"
bind:viewModel="@{viewModel}"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginEnd="9dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toStartOf="@id/today"/>
<ImageView
android:onClick="@{todayClickListener}"
android:id="@+id/today"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="9dp"
android:src="@drawable/calendar"
android:visibility="@{viewModel.searchBarVisible ? View.GONE : View.VISIBLE}"
app:tint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/top_bar"
app:layout_constraintBottom_toBottomOf="@id/top_bar" />
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/top_bar"/>
<ImageView
android:id="@+id/no_meeting_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/illu"
android:layout_margin="10dp"
app:layout_constraintHeight_max="200dp"
app:layout_constraintBottom_toTopOf="@id/no_meeting_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/background" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_meeting_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/meetings_list_empty"
app:layout_constraintBottom_toTopOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/no_meeting_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/meetings_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_meeting"
android:onClick="@{newMeetingClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/plus_circle"
app:tint="@color/gray_main2_700"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.fetchInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,26 @@
<?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="backClickListener"
type="View.OnClickListener" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp"
android:background="@color/white">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,130 @@
<?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="model"
type="org.linphone.ui.main.meetings.model.MeetingModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="firstMeetingOfTheDay"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{onClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/header_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@{firstMeetingOfTheDay ? @dimen/meeting_margin : @dimen/zero, default=@dimen/zero}"
android:layout_marginStart="5dp"
android:text="@{model.day, default=`Mon.`}"
android:visibility="@{firstMeetingOfTheDay ? View.VISIBLE : View.INVISIBLE}"
android:textColor="@color/gray_main2_500"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/header_day_number"
app:layout_constraintEnd_toEndOf="@id/header_day_number"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/today_background"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="5dp"
android:src="@drawable/shape_circle_primary_background"
android:visibility="@{model.isToday &amp;&amp; firstMeetingOfTheDay ? View.VISIBLE : View.INVISIBLE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header_day" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/header_day_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.dayNumber, default=`19`}"
android:visibility="@{firstMeetingOfTheDay ? View.VISIBLE : View.INVISIBLE}"
android:textColor="@{model.isToday ? @color/white : @color/gray_main2_500, default=@color/gray_main2_500}"
android:textSize="20sp"
android:paddingBottom="4dp"
app:layout_constraintStart_toStartOf="@id/today_background"
app:layout_constraintEnd_toEndOf="@id/today_background"
app:layout_constraintBottom_toBottomOf="@id/today_background"
app:layout_constraintTop_toBottomOf="@id/header_day"/>
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="5dp"
android:layout_marginTop="@{firstMeetingOfTheDay ? @dimen/meeting_margin : @dimen/zero, default=@dimen/zero}"
android:layout_marginBottom="8dp"
app:cardElevation="5dp"
app:cardCornerRadius="10dp"
app:layout_constraintStart_toEndOf="@id/header_day"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{model.subject, default=`Meeting with John`}"
android:textSize="13sp"
android:textColor="@color/gray_main2_600"
android:maxLines="1"
android:ellipsize="end"
android:drawableStart="@{model.isBroadcast ? @drawable/slideshow : @drawable/users_three, default=@drawable/users_three}"
android:drawablePadding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="3dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{model.time, default=`10:00 - 12:00`}"
android:textSize="14sp"
android:textColor="@color/gray_main2_500"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,26 @@
<?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="backClickListener"
type="View.OnClickListener" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp"
android:background="@color/white">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,32 @@
<?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" />
</data>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/meetings_list"
android:name="org.linphone.ui.main.meetings.fragment.MeetingsListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_width"
android:layout_height="match_parent"
app:layout="@layout/meetings_list_fragment"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/meetings_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="@dimen/sliding_pane_right_fragment_width"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/meetings_nav_graph"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
</layout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
</data>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/header"
android:background="@color/white"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="October"
android:textSize="20sp"
android:textColor="@color/gray_main2_600"
android:gravity="center_vertical"/>
</layout>

View file

@ -0,0 +1,137 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="newMeetingClicked"
type="View.OnClickListener" />
<variable
name="filterClickListener"
type="View.OnClickListener" />
<variable
name="todayClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.meetings.viewmodel.MeetingsListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/orange_main_500">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_meeting_image, no_meeting_label"
android:visibility="@{viewModel.meetings.empty ? View.VISIBLE : View.GONE}" />
<include
android:id="@+id/top_bar"
layout="@layout/top_bar"
bind:viewModel="@{viewModel}"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginEnd="9dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/today"/>
<ImageView
android:onClick="@{todayClickListener}"
android:id="@+id/today"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="9dp"
android:src="@drawable/calendar"
android:visibility="@{viewModel.searchBarVisible ? View.GONE : View.VISIBLE}"
app:tint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/top_bar"
app:layout_constraintBottom_toBottomOf="@id/top_bar" />
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@color/white"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar" />
<ImageView
android:id="@+id/no_meeting_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/illu"
android:layout_margin="10dp"
app:layout_constraintHeight_max="200dp"
app:layout_constraintBottom_toTopOf="@id/no_meeting_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_meeting_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/meetings_list_empty"
app:layout_constraintBottom_toTopOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_meeting_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/meetings_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/bottom_nav_bar"
android:name="org.linphone.ui.main.fragment.BottomNavBarFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
bind:layout="@layout/bottom_nav_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_meeting"
android:onClick="@{newMeetingClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/plus_circle"
app:tint="@color/gray_main2_700"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.fetchInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -23,6 +23,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/contactsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_contactsFragment_to_meetingsFragment"
app:destination="@id/meetingsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/contactsFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -42,6 +48,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/historyFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_historyFragment_to_meetingsFragment"
app:destination="@id/meetingsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/historyFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -198,6 +210,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/conversationsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_conversationsFragment_to_meetingsFragment"
app:destination="@id/meetingsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/conversationsFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -213,4 +231,42 @@
app:launchSingleTop="true"
app:popExitAnim="@anim/slide_out" />
<fragment
android:id="@+id/meetingsFragment"
android:name="org.linphone.ui.main.meetings.fragment.MeetingsFragment"
android:label="MeetingsFragment"
tools:layout="@layout/meetings_fragment">
<action
android:id="@+id/action_meetingsFragment_to_historyFragment"
app:destination="@id/historyFragment"
app:launchSingleTop="true"
app:popUpTo="@id/meetingsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_meetingsFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/meetingsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_meetingsFragment_to_contactsFragment"
app:destination="@id/contactsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/meetingsFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/scheduleMeetingFragment"
android:name="org.linphone.ui.main.meetings.fragment.ScheduleMeetingFragment"
android:label="ScheduleMeetingFragment"
tools:layout="@layout/meeting_schedule_fragment"/>
<action
android:id="@+id/action_global_scheduleMeetingFragment"
app:destination="@id/scheduleMeetingFragment"
app:enterAnim="@anim/slide_in"
app:launchSingleTop="true"
app:popExitAnim="@anim/slide_out"/>
</navigation>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/meetings_nav_graph"
app:startDestination="@id/emptyFragment">
<fragment
android:id="@+id/emptyFragment"
android:name="org.linphone.ui.main.fragment.EmptyFragment"
android:label="EmptyFragment"
tools:layout="@layout/empty_fragment"/>
<fragment
android:id="@+id/meetingFragment"
android:name="org.linphone.ui.main.meetings.fragment.MeetingFragment"
android:label="MeetingFragment"
tools:layout="@layout/meeting_fragment"/>
<action
android:id="@+id/action_global_meetingFragment"
app:destination="@id/meetingFragment"/>
</navigation>

View file

@ -50,4 +50,6 @@
<dimen name="button_max_width">400dp</dimen>
<dimen name="dialog_max_width">400dp</dimen>
<dimen name="text_input_max_width">400dp</dimen>
<dimen name="meeting_margin">8dp</dimen>
</resources>

View file

@ -331,6 +331,8 @@
<string name="new_conversation_no_contact">No contact for the moment…</string>
<string name="conversation_text_field_hint">Say something…</string>
<string name="meetings_list_empty">No meeting for the moment…</string>
<string name="operation_in_progress_overlay">Operation in progress, please wait</string>
<string name="call_action_blind_transfer">Transfer</string>