diff --git a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt index a038c7195..8b37d553b 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt @@ -14,15 +14,21 @@ 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.MeetingListItemModel import org.linphone.ui.main.meetings.model.MeetingModel import org.linphone.utils.Event import org.linphone.utils.HeaderAdapter class MeetingsListAdapter : - ListAdapter( + ListAdapter( MeetingDiffCallback() ), HeaderAdapter { + companion object { + const val MEETING = 1 + const val TODAY_INDICATOR = 2 + } + var selectedAdapterPosition = -1 val meetingClickedEvent: MutableLiveData> by lazy { @@ -49,13 +55,39 @@ class MeetingsListAdapter : } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + TODAY_INDICATOR -> createTodayIndicatorViewHolder(parent) + else -> createMeetingViewHolder(parent) + } + } + + override fun getItemViewType(position: Int): Int { + val data = getItem(position) + if (data.isToday) { + return TODAY_INDICATOR + } + return MEETING + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is MeetingViewHolder) { + holder.bind(getItem(position).model as MeetingModel) + } + } + + fun resetSelection() { + notifyItemChanged(selectedAdapterPosition) + selectedAdapterPosition = -1 + } + + private fun createMeetingViewHolder(parent: ViewGroup): MeetingViewHolder { val binding: MeetingListCellBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.meeting_list_cell, parent, false ) - val viewHolder = ViewHolder(binding) + val viewHolder = MeetingViewHolder(binding) binding.apply { lifecycleOwner = parent.findViewTreeLifecycleOwner() @@ -73,16 +105,17 @@ class MeetingsListAdapter : return viewHolder } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as ViewHolder).bind(getItem(position)) + private fun createTodayIndicatorViewHolder(parent: ViewGroup): TodayIndicatorViewHolder { + return TodayIndicatorViewHolder( + LayoutInflater.from(parent.context).inflate( + R.layout.meeting_list_today_indicator, + parent, + false + ) + ) } - fun resetSelection() { - notifyItemChanged(selectedAdapterPosition) - selectedAdapterPosition = -1 - } - - inner class ViewHolder( + inner class MeetingViewHolder( val binding: MeetingListCellBinding ) : RecyclerView.ViewHolder(binding.root) { @UiThread @@ -97,13 +130,26 @@ class MeetingsListAdapter : } } - private class MeetingDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MeetingModel, newItem: MeetingModel): Boolean { - return oldItem.id == newItem.id + inner class TodayIndicatorViewHolder( + val view: View + ) : RecyclerView.ViewHolder(view) + + private class MeetingDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MeetingListItemModel, newItem: MeetingListItemModel): Boolean { + if (oldItem.model is MeetingModel && newItem.model is MeetingModel) { + return oldItem.model.id == newItem.model.id + } + return false } - override fun areContentsTheSame(oldItem: MeetingModel, newItem: MeetingModel): Boolean { - return oldItem.subject.value == newItem.subject.value && oldItem.time == newItem.time + override fun areContentsTheSame( + oldItem: MeetingListItemModel, + newItem: MeetingListItemModel + ): Boolean { + if (oldItem.model is MeetingModel && newItem.model is MeetingModel) { + return oldItem.model.subject.value == newItem.model.subject.value && oldItem.model.time == newItem.model.time + } + return false } } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index 5cfcce105..777bbbd14 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -38,7 +38,6 @@ 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.AppUtils import org.linphone.utils.RecyclerViewHeaderDecoration @UiThread @@ -219,14 +218,11 @@ class MeetingsListFragment : AbstractTopBarFragment() { } private fun scrollToToday() { - Log.i("$TAG Scrolling to today's meeting (if any)") val todayMeeting = listViewModel.meetings.value.orEmpty().find { - it.displayTodayIndicator.value == true + it.isToday } val index = listViewModel.meetings.value.orEmpty().indexOf(todayMeeting) - (binding.meetingsList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - index, - AppUtils.getDimension(R.dimen.meeting_list_decoration_height).toInt() - ) + Log.i("$TAG Scrolling to 'today' at position [$index]") + (binding.meetingsList.layoutManager as LinearLayoutManager).scrollToPosition(index) } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingListItemModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingListItemModel.kt new file mode 100644 index 000000000..b4455fd35 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingListItemModel.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ +package org.linphone.ui.main.meetings.model + +import androidx.annotation.WorkerThread +import org.linphone.utils.TimestampUtils + +class MeetingListItemModel @WorkerThread constructor(meetingModel: MeetingModel?) { + val isToday = meetingModel == null + + val month = meetingModel?.month ?: TimestampUtils.month(System.currentTimeMillis(), false) + + val model = meetingModel ?: TodayModel() + + class TodayModel +} diff --git a/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt index 0e3cca069..9e4ee504d 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt @@ -45,6 +45,8 @@ class MeetingModel @WorkerThread constructor(private val conferenceInfo: Confere val isToday = TimestampUtils.isToday(timestamp) + val isAfterToday = TimestampUtils.isAfterToday(timestamp) + private val startTime = TimestampUtils.timeToString(timestamp) private val endTime = TimestampUtils.timeToString(timestamp + (conferenceInfo.duration * 60)) @@ -57,8 +59,6 @@ class MeetingModel @WorkerThread constructor(private val conferenceInfo: Confere val firstMeetingOfTheDay = MutableLiveData() - val displayTodayIndicator = MutableLiveData() - init { subject.postValue(conferenceInfo.subject) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt index 1974a86d9..9c0e017a9 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt @@ -27,16 +27,16 @@ 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.MeetingListItemModel import org.linphone.ui.main.meetings.model.MeetingModel import org.linphone.ui.main.viewmodel.AbstractTopBarViewModel -import org.linphone.utils.TimestampUtils class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() { companion object { private const val TAG = "[Meetings List ViewModel]" } - val meetings = MutableLiveData>() + val meetings = MutableLiveData>() val fetchInProgress = MutableLiveData() @@ -74,7 +74,7 @@ class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() @WorkerThread private fun computeMeetingsList(filter: String) { - val list = arrayListOf() + val list = arrayListOf() var source = coreContext.core.defaultAccount?.conferenceInformationList if (source == null) { @@ -85,7 +85,7 @@ class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() } var previousModel: MeetingModel? = null - var firstMeetingOfTodayFound = false + var meetingForTodayFound = false for (info: ConferenceInfo in source) { val add = if (filter.isNotEmpty()) { val organizerCheck = info.organizer?.asStringUriOnly()?.contains( @@ -101,6 +101,7 @@ class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() } else { true } + if (add) { val model = MeetingModel(info) val firstMeetingOfTheDay = if (previousModel != null) { @@ -110,22 +111,26 @@ class MeetingsListViewModel @UiThread constructor() : AbstractTopBarViewModel() } model.firstMeetingOfTheDay.postValue(firstMeetingOfTheDay) + // Insert "Today" fake model before the first one of today if (firstMeetingOfTheDay && model.isToday) { - firstMeetingOfTodayFound = true - model.displayTodayIndicator.postValue(true) + list.add(MeetingListItemModel(null)) + meetingForTodayFound = true } - list.add(model) + // If no meeting was found for today, insert "Today" fake model before the next meeting to come + if (!meetingForTodayFound && model.isAfterToday) { + list.add(MeetingListItemModel(null)) + meetingForTodayFound = true + } + + list.add(MeetingListItemModel(model)) previousModel = model } } - if (!firstMeetingOfTodayFound) { - val firstMeetingAfterToday = list.find { - TimestampUtils.isAfterToday(it.timestamp) - } - Log.i("$TAG $firstMeetingAfterToday") - firstMeetingAfterToday?.displayTodayIndicator?.postValue(true) + // If no meeting was found after today, insert "Today" fake model at the end + if (!meetingForTodayFound) { + list.add(MeetingListItemModel(null)) } meetings.postValue(list) diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt index 8b15a7648..8700652ab 100644 --- a/app/src/main/java/org/linphone/utils/TimestampUtils.kt +++ b/app/src/main/java/org/linphone/utils/TimestampUtils.kt @@ -110,13 +110,21 @@ class TimestampUtils { fun month(timestamp: Long, timestampInSecs: Boolean = true): String { val calendar = Calendar.getInstance() calendar.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp - return calendar.getDisplayName( + val month = calendar.getDisplayName( Calendar.MONTH, TextStyle.SHORT.ordinal, Locale.getDefault() ) ?.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } ?: "?" + + val now = Calendar.getInstance() + if (isSameYear(now, calendar)) { + return month + } + + val year = calendar.get(Calendar.YEAR) + return "$month $year" } @AnyThread @@ -197,9 +205,9 @@ class TimestampUtils { cal1: Calendar, cal2: Calendar ): Boolean { - return cal1[Calendar.ERA] == cal2[Calendar.ERA] && - cal1[Calendar.YEAR] == cal2[Calendar.YEAR] && - cal1[Calendar.DAY_OF_YEAR] == cal2[Calendar.DAY_OF_YEAR] + return cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) } @AnyThread @@ -207,8 +215,8 @@ class TimestampUtils { cal1: Calendar, cal2: Calendar ): Boolean { - return cal1[Calendar.ERA] == cal2[Calendar.ERA] && - cal1[Calendar.YEAR] == cal2[Calendar.YEAR] + return cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) } } } diff --git a/app/src/main/res/layout/meeting_list_cell.xml b/app/src/main/res/layout/meeting_list_cell.xml index 455b8f0e6..21998c98f 100644 --- a/app/src/main/res/layout/meeting_list_cell.xml +++ b/app/src/main/res/layout/meeting_list_cell.xml @@ -22,26 +22,6 @@ android:paddingStart="16dp" android:paddingEnd="16dp"> - - - - + app:layout_constraintTop_toTopOf="parent"/> + + + + + + + \ No newline at end of file